<?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 · Articles</title>
    <link>https://wilhelm.codes/</link>
    <description>Longer-form articles from Wilhelm Codes.</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    <managingEditor>0xdeadbeef@devilmayco.de (ɯlǝɥlᴉʍ)</managingEditor>
    <webMaster>0xdeadbeef@devilmayco.de (ɯlǝɥlᴉʍ)</webMaster>
    <lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://wilhelm.codes/articles.xml" rel="self" type="application/rss+xml" />
    <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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/why-i-built-glazier/</guid>
      <category>Development</category>
      <category>go</category>
      <category>golang</category>
      <category>tmux</category>
      <category>hcl</category>
      <category>projects</category>
      <description>&lt;p&gt;I live in &lt;a href=&#34;https://github.com/tmux/tmux/wiki&#34;&gt;tmux&lt;/a&gt;. 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, kill the wrong session or just close the laptop lid for too long and it can all evaporates. Then I&amp;rsquo;m back to rebuilding the same layout by hand, one &lt;code&gt;split-window&lt;/code&gt; at a time, like some kind of animal.&lt;/p&gt;</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, kill the wrong session or just close the laptop lid for too long and it can all evaporates. 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"><code>glaze</code></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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>It&rsquo;s just a config file, right?</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 around here, 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>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 glaze 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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#cba6f7">session</span> {
</span></span><span style="display:flex;"><span>  name <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;daemon-run&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">window</span> {
</span></span><span style="display:flex;"><span>    name   <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;ice-breaker&#34;</span>
</span></span><span style="display:flex;"><span>    layout <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;main-vertical&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">pane</span> {
</span></span><span style="display:flex;"><span>      commands <span style="color:#89dceb;font-weight:bold">=</span> [<span style="color:#a6e3a1">&#34;nvim ./payloads&#34;</span>]
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">pane</span> {
</span></span><span style="display:flex;"><span>      commands <span style="color:#89dceb;font-weight:bold">=</span> [<span style="color:#a6e3a1">&#34;watch -n1 netwatch --target arasaka-mainframe&#34;</span>]
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>I&rsquo;m not the first to do this; not even the 3rd&hellip; or the 4th.</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>So what&rsquo;s actually different?</a>
</h2>
<h3 id="the-profile-validates-itself">
  <a class="heading-link" href="#the-profile-validates-itself"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>The profile validates itself!</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Error: Invalid layout specified
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  on .glaze line 4, in session.window:
</span></span><span style="display:flex;"><span>   4:     layout = &#34;main-plumbus&#34;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>The layout value of &#34;main-plumbus&#34; is not a supported preset
</span></span><span style="display:flex;"><span>(even-horizontal, even-vertical, main-horizontal, main-vertical,
</span></span><span style="display:flex;"><span>tiled) nor a valid tmux layout string.
</span></span></code></pre></div><p>There&rsquo;s even 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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Variables, templates and string functions! Oh, my!</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#cba6f7">session</span> {
</span></span><span style="display:flex;"><span>  name               <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;ops-${region}&#34;</span>
</span></span><span style="display:flex;"><span>  starting_directory <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">path</span>.<span style="color:#cba6f7">pwd</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">window</span> {
</span></span><span style="display:flex;"><span>    name <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">upper</span>(<span style="color:#cba6f7">region</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">pane</span> {
</span></span><span style="display:flex;"><span>      commands <span style="color:#89dceb;font-weight:bold">=</span> [<span style="color:#a6e3a1">&#34;k9s --context ${region}&#34;</span>]
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ glaze up --var <span style="color:#f5e0dc">region</span><span style="color:#89dceb;font-weight:bold">=</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>It doesn&rsquo;t lie to you about timing.</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>It can <em>mostly</em> save a session!</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>A real profile.</a>
</h2>
<p>Enough talk. Here&rsquo;s the profile I actually use when working this website:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#cba6f7">session</span> {
</span></span><span style="display:flex;"><span>  name <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">replace</span>(<span style="color:#cba6f7">path</span>.<span style="color:#cba6f7">base</span>, <span style="color:#a6e3a1">&#34;.&#34;, &#34;-&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  envs <span style="color:#89dceb;font-weight:bold">=</span> {
</span></span><span style="display:flex;"><span>    HUGO_GITHUB_TOKEN        <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;${github}&#34;</span>
</span></span><span style="display:flex;"><span>    HUGO_ACTIVITY_GRAPH_DEMO <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;${demo}&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">window</span> {
</span></span><span style="display:flex;"><span>    name  <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;editor&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">pane</span> {
</span></span><span style="display:flex;"><span>      commands <span style="color:#89dceb;font-weight:bold">=</span> [<span style="color:#a6e3a1">&#34;nvim&#34;</span>]
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">window</span> {
</span></span><span style="display:flex;"><span>    name  <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;terminal&#34;</span>
</span></span><span style="display:flex;"><span>    focus <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#f38ba8">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">pane</span> {}
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">window</span> {
</span></span><span style="display:flex;"><span>    name <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;server&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">pane</span> {
</span></span><span style="display:flex;"><span>      commands <span style="color:#89dceb;font-weight:bold">=</span> [<span style="color:#a6e3a1">&#34;hugo server --disableFastRender&#34;</span>]
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">window</span> {
</span></span><span style="display:flex;"><span>    name <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;git&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">pane</span> {
</span></span><span style="display:flex;"><span>      commands <span style="color:#89dceb;font-weight:bold">=</span> [<span style="color:#a6e3a1">&#34;lazygit&#34;</span>]
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</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>One thing I should point out is where I define the session name with <code>replace(path.base, &quot;.&quot;, &quot;-&quot;)</code>. This is a workaround for a small known tmux quirk where it won&rsquo;t allow <code>.</code> or <code>:</code> within a session name. Since the repository is <code>wilhelm.codes</code>, tmux will automatically assign it as <code>wilhelm_codes</code>. And since Glazier refers to the session by the name we explicitly define, it&rsquo;ll lose context the moment the session is created with the following useful error:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>could not create new session <span style="color:#a6e3a1">`</span>wilhelm.codes<span style="color:#a6e3a1">`</span>: session <span style="color:#a6e3a1">`</span>wilhelm.codes<span style="color:#a6e3a1">`</span> was created but could not be found afterwards
</span></span></code></pre></div><p>Definitely a small edge case I should address with the next patch.</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ glaze ls
</span></span><span style="display:flex;"><span>NAME      WINDOWS  PATH
</span></span><span style="display:flex;"><span>glazier*  2        /home/wilhelm/Development/wilhelm.codes
</span></span><span style="display:flex;"><span>scratch   1        /tmp
</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><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-"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Some over-engineered bits &hellip;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Where to get it?</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-console" data-lang="console"><span style="display:flex;"><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-"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>In closing &hellip;</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/let-me-show-you-my-bits/</guid>
      <category>Development</category>
      <category>hugo</category>
      <category>css</category>
      <category>tailwindcss</category>
      <description>&lt;p&gt;A while back, in &lt;a href=&#34;https://wilhelm.codes/blog/reading-between-the-posts/&#34;&gt;Reading Between the Posts&lt;/a&gt;, I wired up the home page so the quiet gaps between articles would narrate themselves - &amp;ldquo;&lt;em&gt;1 bit and 41 changelog events in between&lt;/em&gt;&amp;rdquo;. There was just one tiny problem with that sentence. At the time I had written exactly &lt;em&gt;zero&lt;/em&gt; bits.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>What even is a bit?</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#fab387">---</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">type</span>: quote
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">date</span>: 2026-06-17
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">source</span>: <span style="color:#a6e3a1">&#34;Edsger Dijkstra&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#fab387">---</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#34;Simplicity is a great virtue but it requires hard work to achieve it.&#34;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>One shape per thought.</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>{{ <span style="color:#f38ba8">$</span>meta <span style="color:#89dceb;font-weight:bold">:=</span> dict
</span></span><span style="display:flex;"><span>  <span style="color:#a6e3a1">&#34;quote&#34;</span> (dict <span style="color:#a6e3a1">&#34;icon&#34;</span> <span style="color:#a6e3a1">&#34;quote-right&#34;</span>    <span style="color:#a6e3a1">&#34;colour&#34;</span> <span style="color:#a6e3a1">&#34;rose&#34;</span>    <span style="color:#a6e3a1">&#34;label&#34;</span> <span style="color:#a6e3a1">&#34;quote&#34;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#a6e3a1">&#34;link&#34;</span>  (dict <span style="color:#a6e3a1">&#34;icon&#34;</span> <span style="color:#a6e3a1">&#34;arrow-up-right&#34;</span> <span style="color:#a6e3a1">&#34;colour&#34;</span> <span style="color:#a6e3a1">&#34;blue&#34;</span>    <span style="color:#a6e3a1">&#34;label&#34;</span> <span style="color:#a6e3a1">&#34;link&#34;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#a6e3a1">&#34;video&#34;</span> (dict <span style="color:#a6e3a1">&#34;icon&#34;</span> <span style="color:#a6e3a1">&#34;youtube&#34;</span>        <span style="color:#a6e3a1">&#34;colour&#34;</span> <span style="color:#a6e3a1">&#34;red&#34;</span>     <span style="color:#a6e3a1">&#34;label&#34;</span> <span style="color:#a6e3a1">&#34;video&#34;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#a6e3a1">&#34;blurb&#34;</span> (dict <span style="color:#a6e3a1">&#34;icon&#34;</span> <span style="color:#a6e3a1">&#34;pen-nib&#34;</span>        <span style="color:#a6e3a1">&#34;colour&#34;</span> <span style="color:#a6e3a1">&#34;emerald&#34;</span> <span style="color:#a6e3a1">&#34;label&#34;</span> <span style="color:#a6e3a1">&#34;blurb&#34;</span>)
</span></span><span style="display:flex;"><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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Hugo, why are you like this?</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Bits flapping in the wind.</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>The tag that went nowhere.</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>{{ with site.<span style="color:#89b4fa">GetPage</span> (printf <span style="color:#a6e3a1">&#34;/tags/%s&#34;</span> (urlize .)) }}
</span></span><span style="display:flex;"><span>  &lt;a href=<span style="color:#a6e3a1">&#34;{{ .RelPermalink }}&#34;</span>&gt;<span style="color:#f38ba8">#</span>{{ .Data.Term }}&lt;<span style="color:#89dceb;font-weight:bold">/</span>a&gt;
</span></span><span style="display:flex;"><span>{{ <span style="color:#cba6f7">else</span> }}
</span></span><span style="display:flex;"><span>  &lt;span&gt;<span style="color:#f38ba8">#</span>{{ . }}&lt;<span style="color:#89dceb;font-weight:bold">/</span>span&gt;
</span></span><span style="display:flex;"><span>{{ end }}
</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-"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>In closing &hellip;</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/reading-between-the-posts/</guid>
      <category>Development</category>
      <category>hugo</category>
      <category>css</category>
      <description>&lt;p&gt;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.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Why can&rsquo;t I hold all these events?</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>{{ <span style="color:#f38ba8">$</span>events = <span style="color:#f38ba8">$</span>events | <span style="color:#89b4fa">append</span> (dict
</span></span><span style="display:flex;"><span>  <span style="color:#a6e3a1">&#34;kind&#34;</span>  <span style="color:#a6e3a1">&#34;commit&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e3a1">&#34;date&#34;</span>  (time.AsTime .commit.author.date)
</span></span><span style="display:flex;"><span>  <span style="color:#a6e3a1">&#34;title&#34;</span> (<span style="color:#89b4fa">index</span> (split .commit.message <span style="color:#a6e3a1">&#34;\n&#34;</span>) <span style="color:#fab387">0</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#a6e3a1">&#34;url&#34;</span>   .html_url
</span></span><span style="display:flex;"><span>  <span style="color:#a6e3a1">&#34;icon&#34;</span>  <span style="color:#a6e3a1">&#34;code-commit&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e3a1">&#34;colour&#34;</span> <span style="color:#a6e3a1">&#34;green&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e3a1">&#34;data&#34;</span>  .)
</span></span><span style="display:flex;"><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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Minding the gaps.</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>{{ <span style="color:#f38ba8">$</span>upper <span style="color:#89dceb;font-weight:bold">:=</span> <span style="color:#f38ba8">$</span>now }}
</span></span><span style="display:flex;"><span>{{ <span style="color:#cba6f7">if</span> gt <span style="color:#f38ba8">$</span>i <span style="color:#fab387">0</span> }}{{ <span style="color:#f38ba8">$</span>upper = (index <span style="color:#f38ba8">$</span><span style="color:#89b4fa">articles</span> (sub <span style="color:#f38ba8">$</span>i <span style="color:#fab387">1</span>)).Date }}{{ end }}
</span></span><span style="display:flex;"><span>{{ <span style="color:#cba6f7">range</span> <span style="color:#f38ba8">$</span>e <span style="color:#89dceb;font-weight:bold">:=</span> <span style="color:#f38ba8">$</span>events }}
</span></span><span style="display:flex;"><span>  {{ <span style="color:#cba6f7">if</span> <span style="color:#89b4fa">and</span> (<span style="color:#f38ba8">$</span>e.date.After <span style="color:#f38ba8">$</span>article.Date) (<span style="color:#89b4fa">not</span> (<span style="color:#f38ba8">$</span>e.date.After <span style="color:#f38ba8">$</span>upper)) }}
</span></span><span style="display:flex;"><span>    {{ <span style="color:#f38ba8">$</span>group = <span style="color:#f38ba8">$</span>group | append <span style="color:#f38ba8">$</span>e }}
</span></span><span style="display:flex;"><span>  {{ end }}
</span></span><span style="display:flex;"><span>{{ end }}
</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Listing the interstitial events.</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#cba6f7">details</span> <span style="color:#89b4fa">class</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">&#34;group&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#cba6f7">summary</span>&gt;... 41 changelog events in between ...&lt;/<span style="color:#cba6f7">summary</span>&gt;
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#cba6f7">ul</span>&gt;
</span></span><span style="display:flex;"><span>    <span style="color:#6c7086;font-style:italic">&lt;!-- one little row per event, complete with icon and link --&gt;</span>
</span></span><span style="display:flex;"><span>  &lt;/<span style="color:#cba6f7">ul</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#cba6f7">details</span>&gt;
</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Saving myself a small headache.</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="not-prose mx-10 mb-2 p-0 text-center leading-relaxed text-slate-800 italic sm:text-2xl md:text-4xl"
>
  “Sometimes the right amount of effort is zero and the discipline is in admitting it.”
  
    <cite class="block pt-4 text-center text-sm text-slate-600"
      >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-"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>In closing &hellip;</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/a-year-in-circles/</guid>
      <category>Development</category>
      <category>hugo</category>
      <category>css</category>
      <category>tailwindcss</category>
      <description>&lt;p&gt;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 &lt;em&gt;weeks&lt;/em&gt;. Two rows of twenty-six with one squircle per week, with each squircle coloured a deeper shade of green the busier that week was.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>What counts as a &ldquo;week&rsquo;s worth of activity&rdquo;?</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>{{ <span style="color:#f38ba8">$</span>dates <span style="color:#89dceb;font-weight:bold">:=</span> slice }}
</span></span><span style="display:flex;"><span>{{ <span style="color:#cba6f7">range</span> partial <span style="color:#a6e3a1">&#34;changelog/fetch.html&#34;</span> (dict <span style="color:#a6e3a1">&#34;url&#34;</span> <span style="color:#f38ba8">$</span>commitsUrl <span style="color:#a6e3a1">&#34;fixture&#34;</span> <span style="color:#f38ba8">$</span>fixture) }}
</span></span><span style="display:flex;"><span>  {{ <span style="color:#f38ba8">$</span>dates = <span style="color:#f38ba8">$</span>dates | <span style="color:#89b4fa">append</span> (time.AsTime .commit.author.date) }}
</span></span><span style="display:flex;"><span>{{ end }}
</span></span><span style="display:flex;"><span>{{ <span style="color:#cba6f7">range</span> where site.RegularPages <span style="color:#a6e3a1">&#34;Section&#34;</span> <span style="color:#a6e3a1">&#34;blog&#34;</span> }}
</span></span><span style="display:flex;"><span>  {{ <span style="color:#f38ba8">$</span>dates = <span style="color:#f38ba8">$</span>dates | append .Date }}
</span></span><span style="display:flex;"><span>{{ end }}
</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Sorting a pile of dates into weekly buckets.</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>{{ <span style="color:#f38ba8">$</span>ago <span style="color:#89dceb;font-weight:bold">:=</span> <span style="color:#f38ba8">int</span> (<span style="color:#89b4fa">div</span> (sub now.Unix <span style="color:#f38ba8">$</span>d.Unix) <span style="color:#fab387">604800</span>) }}
</span></span><span style="display:flex;"><span>{{ <span style="color:#cba6f7">if</span> <span style="color:#89b4fa">and</span> (ge <span style="color:#f38ba8">$</span>ago <span style="color:#fab387">0</span>) (lt <span style="color:#f38ba8">$</span>ago <span style="color:#fab387">52</span>) }}
</span></span><span style="display:flex;"><span>  {{ <span style="color:#f38ba8">$</span>counts.<span style="color:#89b4fa">Add</span> (printf <span style="color:#a6e3a1">&#34;%d&#34;</span> (sub <span style="color:#fab387">51</span> <span style="color:#f38ba8">$</span>ago)) <span style="color:#fab387">1</span> }}
</span></span><span style="display:flex;"><span>{{ end }}
</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Painting the squircles.</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>&lt;<span style="color:#cba6f7">div</span> <span style="color:#89b4fa">class</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">&#34;grid grid-cols-26 gap-1.5&#34;</span>&gt;
</span></span><span style="display:flex;"><span>  <span style="color:#6c7086;font-style:italic">&lt;!-- 52 of these green little dudes --&gt;</span>
</span></span><span style="display:flex;"><span>  &lt;<span style="color:#cba6f7">div</span> <span style="color:#89b4fa">class</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">&#34;aspect-square rounded-full bg-emerald-400&#34;</span>&gt;&lt;/<span style="color:#cba6f7">div</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#cba6f7">div</span>&gt;
</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>The graph&rsquo;s painful honesty.</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Drop it in anywhere.</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>{{ partial <span style="color:#a6e3a1">&#34;activity-graph.html&#34;</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/some-long-overdue-housekeeping/</guid>
      <category>Development</category>
      <category>hugo</category>
      <category>tailwindcss</category>
      <category>css</category>
      <category>refactoring</category>
      <description>&lt;p&gt;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 &lt;a href=&#34;https://wilhelm.codes/blog/my-blog-publishing-setup/&#34;&gt;set-and-forget&lt;/a&gt; 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.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Confession Time</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Letting Hugo do the heavy lifting.</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ git diff --shortstat main...modernize
</span></span><span style="display:flex;"><span><span style="color:#fab387">64</span> files changed, <span style="color:#fab387">1872</span> insertions<span style="color:#89dceb;font-weight:bold">(</span>+<span style="color:#89dceb;font-weight:bold">)</span>, <span style="color:#fab387">40197</span> deletions<span style="color:#89dceb;font-weight:bold">(</span>-<span style="color:#89dceb;font-weight:bold">)</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-css" data-lang="css"><span style="display:flex;"><span>@<span style="color:#cba6f7">import</span> <span style="color:#a6e3a1">&#34;tailwindcss&#34;</span>;
</span></span><span style="display:flex;"><span>@<span style="color:#cba6f7">plugin</span> <span style="color:#a6e3a1">&#34;@tailwindcss/typography&#34;</span>;
</span></span><span style="display:flex;"><span>@<span style="color:#cba6f7">source</span> <span style="color:#a6e3a1">&#34;hugo_stats.json&#34;</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>{{<span style="color:#89dceb;font-weight:bold">-</span> <span style="color:#89b4fa">with</span> (templates.<span style="color:#89b4fa">Defer</span> (dict <span style="color:#a6e3a1">&#34;key&#34;</span> <span style="color:#a6e3a1">&#34;css&#34;</span>)) }}
</span></span><span style="display:flex;"><span>  {{<span style="color:#89dceb;font-weight:bold">-</span> with resources.Get <span style="color:#a6e3a1">&#34;css/main.css&#34;</span> }}
</span></span><span style="display:flex;"><span>    {{<span style="color:#89dceb;font-weight:bold">-</span> <span style="color:#f38ba8">$</span>opts <span style="color:#89dceb;font-weight:bold">:=</span> dict <span style="color:#a6e3a1">&#34;minify&#34;</span> (not hugo.IsDevelopment) }}
</span></span><span style="display:flex;"><span>    {{<span style="color:#89dceb;font-weight:bold">-</span> with . | css.TailwindCSS <span style="color:#f38ba8">$</span>opts }}
</span></span><span style="display:flex;"><span>      {{<span style="color:#89dceb;font-weight:bold">-</span> <span style="color:#6c7086;font-style:italic">/* ...do very cool things... */</span> <span style="color:#89dceb;font-weight:bold">-</span>}}
</span></span><span style="display:flex;"><span>    {{<span style="color:#89dceb;font-weight:bold">-</span> end }}
</span></span><span style="display:flex;"><span>  {{<span style="color:#89dceb;font-weight:bold">-</span> end }}
</span></span><span style="display:flex;"><span>{{<span style="color:#89dceb;font-weight:bold">-</span> end }}
</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>So. Many. Deprecations.</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>A Footgun!</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Some honourable mentions.</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Was the juice worth the squeeze?</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/a-changelog-that-builds-itself/</guid>
      <category>Development</category>
      <category>hugo</category>
      <category>github</category>
      <category>api</category>
      <description>&lt;p&gt;This site has a &lt;a href=&#34;https://wilhelm.codes/changelog/&#34;&gt;changelog&lt;/a&gt; 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.&lt;/p&gt;</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>{{ <span style="color:#f38ba8">$</span>url <span style="color:#89dceb;font-weight:bold">:=</span> <span style="color:#a6e3a1">&#34;https://api.github.com/repos/wilhelm-murdoch/wilhelm.codes/events&#34;</span> }}
</span></span><span style="display:flex;"><span>{{ with <span style="color:#89b4fa">try</span> (resources.GetRemote <span style="color:#f38ba8">$</span>url) }}
</span></span><span style="display:flex;"><span>  {{ <span style="color:#f38ba8">$</span>events <span style="color:#89dceb;font-weight:bold">:=</span> .Value | transform.Unmarshal }}
</span></span><span style="display:flex;"><span>  {{<span style="color:#6c7086;font-style:italic">/* ...range over them and render... */</span>}}
</span></span><span style="display:flex;"><span>{{ end }}
</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>&hellip; Then, production happened.</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>ERROR error building site: ... error calling GetRemote:
</span></span><span style="display:flex;"><span>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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>{{ <span style="color:#f38ba8">$</span>opts <span style="color:#89dceb;font-weight:bold">:=</span> dict }}
</span></span><span style="display:flex;"><span>{{ with os.Getenv <span style="color:#a6e3a1">&#34;HUGO_GITHUB_TOKEN&#34;</span> }}
</span></span><span style="display:flex;"><span>  {{ <span style="color:#f38ba8">$</span>opts = dict <span style="color:#a6e3a1">&#34;headers&#34;</span> (dict <span style="color:#a6e3a1">&#34;Authorization&#34;</span> (printf <span style="color:#a6e3a1">&#34;Bearer %s&#34;</span> .)) }}
</span></span><span style="display:flex;"><span>{{ end }}
</span></span><span style="display:flex;"><span>{{ <span style="color:#f38ba8">$</span>remote <span style="color:#89dceb;font-weight:bold">:=</span> <span style="color:#89b4fa">try</span> (resources.GetRemote <span style="color:#f38ba8">$</span>url <span style="color:#f38ba8">$</span>opts) }}
</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Hark! Another Footgun!</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>{{ <span style="color:#cba6f7">if</span> <span style="color:#89b4fa">eq</span> (len .payload.commits) <span style="color:#fab387">1</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>{{ with .payload.commits }}
</span></span><span style="display:flex;"><span>  {{ <span style="color:#cba6f7">if</span> <span style="color:#89b4fa">eq</span> (len .) <span style="color:#fab387">1</span> }}
</span></span><span style="display:flex;"><span>    {{<span style="color:#6c7086;font-style:italic">/* the one-commit case */</span>}}
</span></span><span style="display:flex;"><span>  {{ <span style="color:#cba6f7">else</span> }}
</span></span><span style="display:flex;"><span>    {{<span style="color:#6c7086;font-style:italic">/* the many-commits case */</span>}}
</span></span><span style="display:flex;"><span>  {{ end }}
</span></span><span style="display:flex;"><span>{{ end }}
</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Don&rsquo;t let someone else&rsquo;s server break your build.</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>{{ <span style="color:#f38ba8">$</span>remote <span style="color:#89dceb;font-weight:bold">:=</span> <span style="color:#89b4fa">try</span> (resources.GetRemote <span style="color:#f38ba8">$</span>url <span style="color:#f38ba8">$</span>opts) }}
</span></span><span style="display:flex;"><span>{{ with <span style="color:#f38ba8">$</span>remote.Err }}
</span></span><span style="display:flex;"><span>  {{ warnf <span style="color:#a6e3a1">&#34;changelog: GitHub fetch failed (%s); using the fixture&#34;</span> . }}
</span></span><span style="display:flex;"><span>  {{ <span style="color:#f38ba8">$</span>response = os.ReadFile <span style="color:#a6e3a1">&#34;static/github.json&#34;</span> | transform.Unmarshal }}
</span></span><span style="display:flex;"><span>{{ <span style="color:#cba6f7">else</span> with <span style="color:#f38ba8">$</span>remote.Value }}
</span></span><span style="display:flex;"><span>  {{ <span style="color:#f38ba8">$</span>response = . | transform.Unmarshal }}
</span></span><span style="display:flex;"><span>{{ end }}
</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>In conclusion&hellip;</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/printing-ordinal-numbers-in-hugo/</guid>
      <category>Development</category>
      <category>hugo</category>
      <category>snippets</category>
      <description>&lt;p&gt;I&amp;rsquo;ve been having such a good time building up this website and &lt;a href=&#34;https://gohugo.io&#34;&gt;Hugo&lt;/a&gt; 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.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Shortcodes</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="not-prose mx-10 mb-2 p-0 text-center leading-relaxed text-slate-800 italic sm:text-2xl md:text-4xl"
>
  “Out of all the things I have lost, I miss my mind the most.”
  
    <cite class="block pt-4 text-center text-sm text-slate-600"
      >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="bg-yellow-50 border-b-yellow-200 mb-8 rounded-md border-b-2 p-4"
>
  <div class="flex">
    <div class="ml-2">
      <span
    class="icon text-yellow-600 h-8"
    
    ><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M128 24c-53 0-96 41.19-96 92c0 34.05 19.31 63.78 48 79.69V216a8 8 0 0 0 8 8h80a8 8 0 0 0 8-8v-20.31c28.69-15.91 48-45.64 48-79.69c0-50.81-43-92-96-92M92 152a20 20 0 1 1 20-20a20 20 0 0 1-20 20m72 0a20 20 0 1 1 20-20a20 20 0 0 1-20 20" opacity=".2"/><path d="M92 104a28 28 0 1 0 28 28a28 28 0 0 0-28-28m0 40a12 12 0 1 1 12-12a12 12 0 0 1-12 12m72-40a28 28 0 1 0 28 28a28 28 0 0 0-28-28m0 40a12 12 0 1 1 12-12a12 12 0 0 1-12 12M128 16C70.65 16 24 60.86 24 116c0 34.1 18.27 66 48 84.28V216a16 16 0 0 0 16 16h80a16 16 0 0 0 16-16v-15.72C213.73 182 232 150.1 232 116c0-55.14-46.65-100-104-100m44.12 172.69a8 8 0 0 0-4.12 7V216h-16v-24a8 8 0 0 0-16 0v24h-16v-24a8 8 0 0 0-16 0v24H88v-20.31a8 8 0 0 0-4.12-7C56.81 173.69 40 145.84 40 116c0-46.32 39.48-84 88-84s88 37.68 88 84c0 29.83-16.81 57.69-43.88 72.69"/></g></svg></span
  >
    </div>
    <div class="ml-3">
      <h3 class="text-yellow-600 m-2 mt-0 pt-1 text-lg font-medium">
        Yarr! Ye be warned...
      </h3>
      <div class="text-yellow-400 m-0 ml-2 text-sm font-light">
        You should probably pay attention.
      </div>
    </div>
  </div>
</div>







  
  
  



<div
  class="bg-green-50 border-b-green-200 mb-8 rounded-md border-b-2 p-4"
>
  <div class="flex">
    <div class="ml-2">
      <span
    class="icon text-green-600 h-8"
    
    ><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M216 116v36a80 80 0 0 1-80 80c-44.18 0-55.81-24-93.32-90a20 20 0 0 1 34.64-20L96 152V44a20 20 0 0 1 40 0v56a20 20 0 0 1 40 0v16a20 20 0 0 1 40 0" opacity=".2"/><path d="M196 88a27.86 27.86 0 0 0-13.35 3.39A28 28 0 0 0 144 74.7V44a28 28 0 0 0-56 0v80l-3.82-6.13A28 28 0 0 0 35.73 146l4.67 8.23C74.81 214.89 89.05 240 136 240a88.1 88.1 0 0 0 88-88v-36a28 28 0 0 0-28-28m12 64a72.08 72.08 0 0 1-72 72c-37.63 0-47.84-18-81.68-77.68l-4.69-8.27V138A12 12 0 0 1 54 121.61a11.9 11.9 0 0 1 6-1.6a12 12 0 0 1 10.41 6a2 2 0 0 0 .14.23l18.67 30A8 8 0 0 0 104 152V44a12 12 0 0 1 24 0v68a8 8 0 0 0 16 0v-12a12 12 0 0 1 24 0v20a8 8 0 0 0 16 0v-4a12 12 0 0 1 24 0Z"/></g></svg></span
  >
    </div>
    <div class="ml-3">
      <h3 class="text-green-600 m-2 mt-0 pt-1 text-lg font-medium">
        Check this out!
      </h3>
      <div class="text-green-400 m-0 ml-2 text-sm font-light">
        This may be of some small interest.
      </div>
    </div>
  </div>
</div>







  
  
  



<div
  class="bg-red-50 border-b-red-200 mb-8 rounded-md border-b-2 p-4"
>
  <div class="flex">
    <div class="ml-2">
      <span
    class="icon text-red-600 h-8"
    
    ><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M192 160A80 80 0 1 1 80 86.66V72a8 8 0 0 1 8-8h48a8 8 0 0 1 8 8v14.66A80 80 0 0 1 192 160" opacity=".2"/><path d="M248 32a8 8 0 0 0-8 8a52.7 52.7 0 0 1-3.57 17.39C232.38 67.22 225.7 72 216 72c-11.06 0-18.85-9.76-29.49-24.65C176 32.66 164.12 16 144 16c-16.39 0-29 8.89-35.43 25a66 66 0 0 0-3.9 15H88a16 16 0 0 0-16 16v9.59A88 88 0 0 0 112 248h1.59A88 88 0 0 0 152 81.59V72a16 16 0 0 0-16-16h-15.12a46.8 46.8 0 0 1 2.69-9.37C127.62 36.78 134.3 32 144 32c11.06 0 18.85 9.76 29.49 24.65C184 71.34 195.88 88 216 88c16.39 0 29-8.89 35.43-25A68.7 68.7 0 0 0 256 40a8 8 0 0 0-8-8M140.8 94a72 72 0 1 1-57.6 0a8 8 0 0 0 4.8-7.34V72h48v14.66a8 8 0 0 0 4.8 7.34m-28.91 115.32A8 8 0 0 1 104 216a8.5 8.5 0 0 1-1.33-.11a57.5 57.5 0 0 1-46.57-46.57a8 8 0 1 1 15.78-2.64a41.29 41.29 0 0 0 33.43 33.43a8 8 0 0 1 6.58 9.21"/></g></svg></span
  >
    </div>
    <div class="ml-3">
      <h3 class="text-red-600 m-2 mt-0 pt-1 text-lg font-medium">
        5... 4... 3... 2...
      </h3>
      <div class="text-red-400 m-0 ml-2 text-sm font-light">
        Yeah, so, we&rsquo;re all gonna die.
      </div>
    </div>
  </div>
</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Partials</a>
</h2>






  
  
  



<div
  class="bg-blue-50 border-b-blue-200 mb-8 rounded-md border-b-2 p-4"
>
  <div class="flex">
    <div class="ml-2">
      <span
    class="icon text-blue-600 h-8"
    
    ><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M208 129v39H48v-40a80 80 0 0 1 80.61-80c44.11.33 79.39 36.89 79.39 81" opacity=".2"/><path d="M120 16V8a8 8 0 0 1 16 0v8a8 8 0 0 1-16 0m80 32a8 8 0 0 0 5.66-2.34l8-8a8 8 0 0 0-11.32-11.32l-8 8A8 8 0 0 0 200 48M50.34 45.66a8 8 0 0 0 11.32-11.32l-8-8a8 8 0 0 0-11.32 11.32Zm87 26.45a8 8 0 1 0-2.64 15.78C153.67 91.08 168 108.32 168 128a8 8 0 0 0 16 0c0-27.4-20.07-51.43-46.68-55.89ZM232 176v24a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16v-24a16 16 0 0 1 16-16v-32a88 88 0 0 1 88-88h.68c48.15.36 87.33 40.29 87.33 89v31A16 16 0 0 1 232 176M56 160h144v-31c0-40-32.05-72.71-71.45-73H128a72 72 0 0 0-72 72Zm160 40v-24H40v24z"/></g></svg></span
  >
    </div>
    <div class="ml-3">
      <h3 class="text-blue-600 m-2 mt-0 pt-1 text-lg font-medium">
        For your information, I&#39;ll have you know...
      </h3>
      <div class="text-blue-400 m-0 ml-2 text-sm font-light">
        &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>
    </div>
  </div>
</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>{{- if and ( eq ( mod . 10 ) 1 ) ( ne ( mod . 100 ) 11 ) -}}
</span></span><span style="display:flex;"><span>    st
</span></span><span style="display:flex;"><span>{{ else if and ( eq ( mod . 10 ) 2 ) ( ne ( mod . 100 ) 12 ) -}}
</span></span><span style="display:flex;"><span>    nd
</span></span><span style="display:flex;"><span>{{ else if and ( eq ( mod . 10 ) 3 ) ( ne ( mod . 100 ) 13 ) -}}
</span></span><span style="display:flex;"><span>    rd
</span></span><span style="display:flex;"><span>{{ else -}}
</span></span><span style="display:flex;"><span>    th
</span></span><span style="display:flex;"><span>{{ 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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#cba6f7">const</span> number <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#fab387">10</span>
</span></span><span style="display:flex;"><span><span style="color:#f38ba8">let</span> ordinal <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;th&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">if</span> (number <span style="color:#89dceb;font-weight:bold">%</span> <span style="color:#fab387">10</span> <span style="color:#89dceb;font-weight:bold">==</span> <span style="color:#fab387">1</span> <span style="color:#89dceb;font-weight:bold">&amp;&amp;</span> number <span style="color:#89dceb;font-weight:bold">%</span> <span style="color:#fab387">100</span> <span style="color:#89dceb;font-weight:bold">!=</span> <span style="color:#fab387">11</span>) {
</span></span><span style="display:flex;"><span>	ordinal <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;st&#34;</span>
</span></span><span style="display:flex;"><span>} <span style="color:#cba6f7">else</span> <span style="color:#cba6f7">if</span> (number <span style="color:#89dceb;font-weight:bold">%</span> <span style="color:#fab387">10</span> <span style="color:#89dceb;font-weight:bold">==</span> <span style="color:#fab387">2</span> <span style="color:#89dceb;font-weight:bold">&amp;&amp;</span> number <span style="color:#89dceb;font-weight:bold">%</span> <span style="color:#fab387">100</span> <span style="color:#89dceb;font-weight:bold">!=</span> <span style="color:#fab387">12</span>) {
</span></span><span style="display:flex;"><span>	ordinal <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;nd&#34;</span>
</span></span><span style="display:flex;"><span>} <span style="color:#cba6f7">else</span> <span style="color:#cba6f7">if</span> (number <span style="color:#89dceb;font-weight:bold">%</span> <span style="color:#fab387">10</span> <span style="color:#89dceb;font-weight:bold">==</span> <span style="color:#fab387">3</span> <span style="color:#89dceb;font-weight:bold">&amp;&amp;</span> number <span style="color:#89dceb;font-weight:bold">%</span> <span style="color:#fab387">100</span> <span style="color:#89dceb;font-weight:bold">!=</span> <span style="color:#fab387">13</span>) {
</span></span><span style="display:flex;"><span>	ordinal <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;rd&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>console.log(number <span style="color:#89dceb;font-weight:bold">+</span> ordinal)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// 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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>{{ 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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-html" data-lang="html"><span style="display:flex;"><span>{{ range seq 10 }}
</span></span><span style="display:flex;"><span>  {{.}}{{ partial &#34;functions/ordinal.html&#34; . }} 
</span></span><span style="display:flex;"><span>{{ end }}
</span></span></code></pre></div><p>Which generates:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/my-blog-publishing-setup/</guid>
      <category>Development</category>
      <category>obsidian</category>
      <category>hugo</category>
      <category>bash</category>
      <category>cloudflare</category>
      <description>&lt;p&gt;Late last year I parted ways with &lt;a href=&#34;https://hashnode.com/&#34;&gt;Hashnode&lt;/a&gt; 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 &lt;a href=&#34;https://en.wikipedia.org/wiki/Ouroboros&#34;&gt;Ouroboros&lt;/a&gt;, if you will. Good luck with that, but I&amp;rsquo;ll pass thanks.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>First, a Small Rant</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>The &ldquo;Stack&rdquo;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Obsidian</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Hugo</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Cloudflare</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>The Good Stuff</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">#!/usr/bin/env bash
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f5e0dc">SOURCE</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SOURCE</span>:=<span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">1</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">}</span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb">export</span> SOURCE
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f5e0dc">DESTINATION</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">DESTINATION</span>:=<span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">2</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">}</span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb">export</span> DESTINATION
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>fswatch -o <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SOURCE</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | <span style="color:#cba6f7">while</span> <span style="color:#89dceb">read</span> -r event; <span style="color:#cba6f7">do</span> 
</span></span><span style="display:flex;"><span>    rsync -av --delete <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SOURCE</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">DESTINATION</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ ./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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>No Auto-Commit?</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ git add -A
</span></span><span style="display:flex;"><span>$ git commit -m <span style="color:#a6e3a1">&#39;some banal new bullshit screed&#39;</span>
</span></span><span style="display:flex;"><span>$ 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-"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>In Closing &hellip;</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/breaking-up-log-output-with-bash/</guid>
      <category>Development</category>
      <category>bash</category>
      <category>snippets</category>
      <category>tutorials</category>
      <description>&lt;p&gt;I tend to look at a &lt;em&gt;lot&lt;/em&gt; 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.&lt;/p&gt;</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">#!/usr/bin/env bash
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f5e0dc">lines</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#fab387">0</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">while</span> <span style="color:#f5e0dc">IFS</span><span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#89dceb">read</span> -r line; <span style="color:#cba6f7">do</span>
</span></span><span style="display:flex;"><span>	<span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">line</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#89dceb;font-weight:bold">((</span>lines++<span style="color:#89dceb;font-weight:bold">))</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#cba6f7">if</span> <span style="color:#89dceb;font-weight:bold">((</span>lines % <span style="color:#f5e0dc">5</span> <span style="color:#89dceb;font-weight:bold">==</span> 0<span style="color:#89dceb;font-weight:bold">))</span>; <span style="color:#cba6f7">then</span>
</span></span><span style="display:flex;"><span>		<span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;----------&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#cba6f7">fi</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ <span style="color:#cba6f7">while</span> true; <span style="color:#cba6f7">do</span> <span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;emitting a noop&#34;</span>; sleep 1; <span style="color:#cba6f7">done</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>emitting a noop
</span></span><span style="display:flex;"><span>emitting a noop
</span></span><span style="display:flex;"><span>emitting a noop
</span></span><span style="display:flex;"><span>emitting a noop
</span></span><span style="display:flex;"><span>emitting a noop
</span></span><span style="display:flex;"><span>----------
</span></span><span style="display:flex;"><span>emitting a noop
</span></span><span style="display:flex;"><span>emitting a noop
</span></span><span style="display:flex;"><span>emitting a noop
</span></span><span style="display:flex;"><span>emitting a noop
</span></span><span style="display:flex;"><span>emitting a noop
</span></span><span style="display:flex;"><span>----------
</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/how-to-mass-unfollow-instagram-accounts/</guid>
      <category>Happy Hacking</category>
      <category>bash</category>
      <category>api</category>
      <category>tutorials</category>
      <category>snippets</category>
      <description>&lt;p&gt;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 &lt;a href=&#34;https://bsky.app/profile/wilhelm.codes&#34;&gt;Bluesky&lt;/a&gt; at the moment. Who knows how long that will last. Having recently read about Meta creating loads of &lt;a href=&#34;https://www.404media.co/metas-ai-profiles-are-indistinguishable-from-terrible-spam-that-took-over-facebook/&#34;&gt;fake AI-based accounts&lt;/a&gt;, I thought it was time to do some spring cleaning.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Before we begin</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Requirements</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Fire up the browser!</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Your following list</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>An unfollow request</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Putting it all together</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f5e0dc">ids</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>
</span></span><span style="display:flex;"><span>  curl <span style="color:#a6e3a1">&#39;https://www.instagram.com/api/v1/friendships/.../following/?count=100&amp;hl=en&#39;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;cookie: ...&#39;</span>           <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;x-asbd-id: ...&#39;</span>        <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;x-csrftoken: ...&#39;</span>      <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;x-ig-app-id: ...&#39;</span>      <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;x-ig-www-claim: ...&#39;</span>   <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;x-requested-with: ...&#39;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;x-web-session-id: ...&#39;</span> | jq -r <span style="color:#a6e3a1">&#39;.users[].id&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">while</span> <span style="color:#f5e0dc">IFS</span><span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#89dceb">read</span> -r id; <span style="color:#cba6f7">do</span>
</span></span><span style="display:flex;"><span>  <span style="color:#89dceb">echo</span> -n <span style="color:#a6e3a1">&#34;Unfollowing user &#39;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">id</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#39;&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  curl -s -o /dev/null <span style="color:#a6e3a1">&#34;https://www.instagram.com/api/v1/friendships/destroy/</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">id</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">/?hl=en&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;cookie: ...&#39;</span>           <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;x-asbd-id: ...&#39;</span>        <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;x-csrftoken: ...&#39;</span>      <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;x-ig-app-id: ...&#39;</span>      <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;x-ig-www-claim: ...&#39;</span>   <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;x-requested-with: ...&#39;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -H <span style="color:#a6e3a1">&#39;x-web-session-id: ...&#39;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    --data-raw <span style="color:#a6e3a1">&#34;container_module=profile&amp;nav_chain=PolarisProfilePostsTabRoot%3AprofilePage%3A1%3Avia_cold_start%2CPolarisProfilePostsTabRoot%3AprofilePage%3A2%3Aunexpected&amp;user_id=</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">id</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#39; ... done!&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">done</span> <span style="color:#89dceb;font-weight:bold">&lt;&lt;&lt;</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">ids</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Unfollowing user &#39;111&#39;... done
</span></span><span style="display:flex;"><span>Unfollowing user &#39;222&#39;... done
</span></span><span style="display:flex;"><span>Unfollowing user &#39;333&#39;... done
</span></span><span style="display:flex;"><span>Unfollowing user &#39;444&#39;... done
</span></span><span style="display:flex;"><span>Unfollowing user &#39;555&#39;... done
</span></span><span style="display:flex;"><span>Unfollowing user &#39;666&#39;... done
</span></span><span style="display:flex;"><span>Unfollowing user &#39;777&#39;... done
</span></span><span style="display:flex;"><span>Unfollowing user &#39;888&#39;... done
</span></span><span style="display:flex;"><span>Finished
</span></span></code></pre></div><h3 id="caveats-gotchas--other-things">
  <a class="heading-link" href="#caveats-gotchas--other-things"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Caveats, gotchas &amp; other things</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-"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>In closing &hellip;</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/the-longer-something-doesnt-happen-the-sooner-it-will/</guid>
      <category>Platform Engineering</category>
      <category>devops</category>
      <category>sre</category>
      <category>chaos-engineering</category>
      <description>&lt;p&gt;This is often referred to as the &amp;ldquo;&lt;a href=&#34;https://en.wikipedia.org/wiki/Mean_time_between_failures&#34;&gt;Mean Time Between Failures (MTBF)&lt;/a&gt;&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.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Accumulation of Underlying Issues</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Complacency and Reduced Vigilance</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Evolving Environments</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Regression to the Mean</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Maintaining Awareness</a>
</h2>
<blockquote
  class="not-prose mx-10 mb-2 p-0 text-center leading-relaxed text-slate-800 italic sm:text-2xl md:text-4xl"
>
  “The price of <s>freedom</s> stability is eternal vigilance.”
  
    <cite class="block pt-4 text-center text-sm text-slate-600"
      >Ancient Klingon Proverb ( probably )</cite
    >
  
</blockquote>

<h3 id="mitigation-strategies">
  <a class="heading-link" href="#mitigation-strategies"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Mitigation Strategies</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Risk Management</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Continuous improvement</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Metrics &amp; Monitoring</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>In Conclusion</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/why-i-built-plant-smart/</guid>
      <category>Thoughts &amp; Musings</category>
      <category>svelte</category>
      <category>projects</category>
      <description>&lt;p&gt;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.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>It&rsquo;s as simple as that?</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>A challenge!</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>The Requirements:</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Any Caveats?</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Where did you source all this data?</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Where to go from here?</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/learning-bash-through-pointless-fun/</guid>
      <category>Development</category>
      <category>bash</category>
      <description>&lt;p&gt;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 &lt;em&gt;mean&lt;/em&gt; to sound sarcastic?&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Here We Go!</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">#!/usr/bin/env bash
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb">set</span> -eo pipefail
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb">read</span> text
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">for</span> <span style="color:#89dceb;font-weight:bold">((</span> <span style="color:#f5e0dc">i</span><span style="color:#89dceb;font-weight:bold">=</span>0; i &lt; <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${#</span><span style="color:#f5e0dc">text</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span>; i++ <span style="color:#89dceb;font-weight:bold">))</span>; <span style="color:#cba6f7">do</span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">if</span> <span style="color:#89dceb;font-weight:bold">[[</span> <span style="color:#cba6f7">$((</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">RANDOM</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#89dceb;font-weight:bold">%</span> <span style="color:#fab387">2</span> <span style="color:#cba6f7">))</span> -eq <span style="color:#fab387">0</span> <span style="color:#89dceb;font-weight:bold">]]</span>; <span style="color:#cba6f7">then</span>
</span></span><span style="display:flex;"><span>    <span style="color:#89dceb">echo</span> -n <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">text</span>:<span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">i</span><span style="color:#a6e3a1">}</span>:<span style="color:#f5e0dc">1</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | tr <span style="color:#a6e3a1">&#39;[:lower:]&#39;</span> <span style="color:#a6e3a1">&#39;[:upper:]&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">else</span>
</span></span><span style="display:flex;"><span>    <span style="color:#89dceb">echo</span> -n <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">text</span>:<span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">i</span><span style="color:#a6e3a1">}</span>:<span style="color:#f5e0dc">1</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | tr <span style="color:#a6e3a1">&#39;[:upper:]&#39;</span> <span style="color:#a6e3a1">&#39;[:lower:]&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">fi</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">done</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">#!/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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#89dceb">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;wilhelm, i asked you to patch the server.&#34;</span> | ./spongebob
</span></span><span style="display:flex;"><span>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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#89dceb">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#cba6f7">for</span> <span style="color:#89dceb;font-weight:bold">((</span> <span style="color:#f5e0dc">i</span><span style="color:#89dceb;font-weight:bold">=</span>0; i &lt; <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${#</span><span style="color:#f5e0dc">text</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span>; i++ <span style="color:#89dceb;font-weight:bold">))</span>; <span style="color:#cba6f7">do</span>
</span></span><span style="display:flex;"><span>  <span style="color:#6c7086;font-style:italic"># ... sweet code goes here </span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#cba6f7">if</span> <span style="color:#89dceb;font-weight:bold">[[</span> <span style="color:#cba6f7">$((</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">RANDOM</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#89dceb;font-weight:bold">%</span> <span style="color:#fab387">2</span> <span style="color:#cba6f7">))</span> -eq <span style="color:#fab387">0</span> <span style="color:#89dceb;font-weight:bold">]]</span>; <span style="color:#cba6f7">then</span>
</span></span><span style="display:flex;"><span>  <span style="color:#6c7086;font-style:italic"># ... do something</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">else</span>
</span></span><span style="display:flex;"><span>  <span style="color:#6c7086;font-style:italic"># ... do the opposite</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#89dceb">echo</span> -n <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">text</span>:<span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">i</span><span style="color:#a6e3a1">}</span>:<span style="color:#f5e0dc">1</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#89dceb">echo</span> -n <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">text</span>:<span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">i</span><span style="color:#a6e3a1">}</span>:<span style="color:#f5e0dc">1</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | tr <span style="color:#a6e3a1">&#39;[:lower:]&#39;</span> <span style="color:#a6e3a1">&#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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#89dceb">echo</span> -n <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">text</span>:<span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">i</span><span style="color:#a6e3a1">}</span>:<span style="color:#f5e0dc">1</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | tr <span style="color:#a6e3a1">&#39;[:upper:]&#39;</span> <span style="color:#a6e3a1">&#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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Testing Time</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ <span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;abandon all hope, ye who enter here.&#34;</span> | ./spongebob
</span></span><span style="display:flex;"><span>abaNDON ALl Hope, yE wHO EnTer heRE.
</span></span><span style="display:flex;"><span>$ <span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;Wilhelm, that last deployment failed. Could you roll it back, please?&#34;</span> | ./spongebob
</span></span><span style="display:flex;"><span>WilhelM, THAT LASt dEployMeNT fAILED. coUlD YOu RoLL It BaCk, PlEaSE?
</span></span><span style="display:flex;"><span>$ <span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;Wilhelm, I am your manager. Please, stop mocking me.&#34;</span> | ./spongebob
</span></span><span style="display:flex;"><span>wIlHElM, i aM yOUr MANagER. PlEasE, sTOP mOCKINg mE.
</span></span><span style="display:flex;"><span>$ <span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;Wilhelm, should we use Kubernetes for this?&#34;</span> | ./spongebob
</span></span><span style="display:flex;"><span>WilHELM, should We Use KuBerNeTEs <span style="color:#cba6f7">for</span> THiS?
</span></span></code></pre></div><h2 id="in-conclusion">
  <a class="heading-link" href="#in-conclusion"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>In Conclusion…</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="not-prose mx-10 mb-2 p-0 text-center leading-relaxed text-slate-800 italic sm:text-2xl md:text-4xl"
>
  “DefInIteLY dO Not UsE THis nEw knoWleDGE to FRUStRATe aND aNnOY YoUR cOWorkers. pLeAsE, be ConsiDeRAtE oF oThEr peOPle’s mEnTal well beiNG.”
  
    <cite class="block pt-4 text-center text-sm text-slate-600"
      >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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/filtering-docker-containers-with-jq/</guid>
      <category>Platform Engineering</category>
      <category>docker</category>
      <category>js</category>
      <category>json</category>
      <category>bash</category>
      <description>&lt;p&gt;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.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Setting Up</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>apt-get install jq
</span></span></code></pre></div><p>Or, on MacOS:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>brew update <span style="color:#89dceb;font-weight:bold">&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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#cba6f7">for</span> i in <span style="color:#89dceb;font-weight:bold">{</span>1..5<span style="color:#89dceb;font-weight:bold">}</span>; <span style="color:#cba6f7">do</span> docker run -it --rm -d redis; <span style="color:#cba6f7">done</span>
</span></span></code></pre></div><h2 id="environmental-variables">
  <a class="heading-link" href="#environmental-variables"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Environmental Variables</a>
</h2>
<h3 id="sourceable-blocks">
  <a class="heading-link" href="#sourceable-blocks"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Sourceable Blocks</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    .[].Config.Env
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | flatten 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | .[]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">	[
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">	  .[].Config.Env
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">	] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | flatten 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | unique 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | .[]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#39;</span>
</span></span></code></pre></div><p>What if I want a set from specific containers?</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    [
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">	    .[] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">      | select([.Id] | inside([
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">	      &#34;747c2c92855f88a8e9aa9709dcdf3a01f1677e70bf4c0bf0e520eb38ac502876&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">	      &#34;2cf70261ef62bc36d19aba02f8ef7b8d7aabfcb1e9f4593a919baa17f80aca5b&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">	    ]))
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">      | .Config.Env
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">	] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | flatten 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | unique 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | .[]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    .[] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | select(.NetworkSettings.Networks.bridge.IPAddress == &#34;172.17.0.2&#34;) 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | .Config.Env 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | .[]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#39;</span>
</span></span></code></pre></div><p>Multiple ip addresses?</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    .[] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | select([.NetworkSettings.Networks.bridge.IPAddress] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | inside([
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">      &#34;172.17.0.2&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">      &#34;172.17.0.3&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    ])) 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | .Config.Env 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | .[]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#39;</span>
</span></span></code></pre></div><h3 id="as-flags-instead">
  <a class="heading-link" href="#as-flags-instead"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>As Flags Instead</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    [
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">        [
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">          .[].Config.Env
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">        ] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">      | flatten 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">      | unique 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">      | &#34;--env=\(.[])&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    ] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | join(&#34; &#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>--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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Filtering By Relative Time</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>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-"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Newer Than …</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f5e0dc">threshold</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>date -d <span style="color:#a6e3a1">&#34;1 hour ago&#34;</span> +%s<span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq --argjson threshold <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">threshold</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    .[] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | select((.State.StartedAt | split(&#34;.&#34;)[0] | &#34;\(.)Z&#34; | fromdate) &gt; $threshold)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f5e0dc">threshold</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>date -d <span style="color:#a6e3a1">&#34;1 hour ago&#34;</span> +%s<span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq --argjson threshold <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">threshold</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    .[] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | select((.State.StartedAt | split(&#34;.&#34;)[0] | &#34;\(.)Z&#34; | fromdate) &gt; $threshold)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | .Name[1:]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f5e0dc">threshold</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>date -d <span style="color:#a6e3a1">&#34;1 hour ago&#34;</span> +%s<span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq --argjson threshold <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">threshold</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    .[] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | select((.State.StartedAt | split(&#34;.&#34;)[0] | &#34;\(.)Z&#34; | fromdate) &gt; $threshold)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | .Id
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#39;</span>
</span></span></code></pre></div><p>Or, ip addresses:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f5e0dc">threshold</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>date -d <span style="color:#a6e3a1">&#34;1 hour ago&#34;</span> +%s<span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq --argjson threshold <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">threshold</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    .[] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | select((.State.StartedAt | split(&#34;.&#34;)[0] | &#34;\(.)Z&#34; | fromdate) &gt; $threshold)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | .NetworkSettings.Networks.bridge.IPAddress
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#39;</span>
</span></span></code></pre></div><h3 id="older-than">
  <a class="heading-link" href="#older-than"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Older Than???</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f5e0dc">threshold</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>date -d <span style="color:#a6e3a1">&#34;1 hour ago&#34;</span> +%s<span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq --argjson threshold <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">threshold</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    .[] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | select((.State.StartedAt | split(&#34;.&#34;)[0] | &#34;\(.)Z&#34; | fromdate) &lt; $threshold)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#39;</span>
</span></span></code></pre></div><h2 id="other-common-patterns">
  <a class="heading-link" href="#other-common-patterns"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Other Common Patterns</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  .[].NetworkSettings.Networks.bridge.IPAddress
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#39;</span>
</span></span></code></pre></div><p>The output would look something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>172.17.0.6
</span></span><span style="display:flex;"><span>172.17.0.5
</span></span><span style="display:flex;"><span>172.17.0.4
</span></span><span style="display:flex;"><span>172.17.0.3
</span></span><span style="display:flex;"><span>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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">	.[]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | select(.NetworkSettings.Networks.bridge.IPAddress == &#34;172.17.0.3&#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | .Id
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  .[].Name[1:]
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#39;</span>
</span></span></code></pre></div><p>Which would give you something like:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>zealous_hertz
</span></span><span style="display:flex;"><span>fervent_dijkstra
</span></span><span style="display:flex;"><span>focused_galileo
</span></span><span style="display:flex;"><span>zealous_poincare
</span></span><span style="display:flex;"><span>naughty_wescoff
</span></span></code></pre></div><h2 id="cleaning-up-">
  <a class="heading-link" href="#cleaning-up-"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Cleaning Up …</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker <span style="color:#89dceb">kill</span> <span style="color:#cba6f7">$(</span>docker inspect <span style="color:#cba6f7">$(</span>docker ps -q<span style="color:#cba6f7">)</span> | jq -r <span style="color:#a6e3a1">&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">	.[] 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | select(.Config.Image == &#34;redis&#34;) 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">  | .Id
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">&#39;</span><span style="color:#cba6f7">)</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-"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>In Closing …</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>But, Wait! There’s More!</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>dq older-than <span style="color:#fab387">4</span> months --only-return ips --network pebkac
</span></span><span style="display:flex;"><span>dq newer-than <span style="color:#fab387">2</span> fortnights --only-return ids
</span></span><span style="display:flex;"><span>dq filter <span style="color:#a6e3a1">&#39;.[].Name[1:]&#39;</span>
</span></span><span style="display:flex;"><span>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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/why-cant-i-hold-all-these-slack-emojis/</guid>
      <category>Random Bullshit</category>
      <category>slack</category>
      <category>emoji</category>
      <category>bash</category>
      <category>api</category>
      <description>&lt;p&gt;Previously, &lt;a href=&#34;https://wilhelm.codes/liberating-custom-slack-emojis&#34;&gt;last blog post&lt;/a&gt; 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 &lt;em&gt;new&lt;/em&gt; Slack workspace?&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Gathering Requirements</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Testing Your Findings</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -s --compressed <span style="color:#a6e3a1">&#34;https://&lt;domain&gt;.slack.com/api/emoji.add&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>  -H <span style="color:#a6e3a1">&#39;content-type: multipart/form-data&#39;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>  -H <span style="color:#a6e3a1">&#34;cookie: d=&lt;cookie&gt;;&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>  -F <span style="color:#a6e3a1">&#34;token=&lt;token&gt;&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>  -F <span style="color:#a6e3a1">&#34;name=&lt;name&gt;&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>  -F <span style="color:#f5e0dc">mode</span><span style="color:#89dceb;font-weight:bold">=</span>data <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>  -F <span style="color:#a6e3a1">&#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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{ <span style="color:#cba6f7">&#34;ok&#34;</span>: <span style="color:#fab387">true</span> }
</span></span></code></pre></div><p>And, if something went wrong:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{ 
</span></span><span style="display:flex;"><span>	<span style="color:#cba6f7">&#34;ok&#34;</span>: <span style="color:#fab387">false</span>,
</span></span><span style="display:flex;"><span>	<span style="color:#cba6f7">&#34;error&#34;</span>: <span style="color:#a6e3a1">&#34;err_code&#34;</span>
</span></span><span style="display:flex;"><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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>The Implementation</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>: <span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_COOKIE</span>:=<span style="color:#a6e3a1">}</span>
</span></span><span style="display:flex;"><span>: <span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_TOKEN</span>:=<span style="color:#a6e3a1">}</span>
</span></span><span style="display:flex;"><span>: <span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_DOMAIN</span>:=<span style="color:#a6e3a1">}</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f5e0dc">files</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>find . -iname <span style="color:#89b4fa">\*</span>.gif -o -iname <span style="color:#89b4fa">\*</span>.png -o -iname <span style="color:#89b4fa">\*</span>.png -maxdepth 1<span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb;font-weight:bold">[[</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">files</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#89dceb;font-weight:bold">==</span> <span style="color:#a6e3a1">&#34;&#34;</span> <span style="color:#89dceb;font-weight:bold">]]</span> <span style="color:#89dceb;font-weight:bold">&amp;&amp;</span> <span style="color:#89dceb">exit</span> <span style="color:#fab387">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">files</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | <span style="color:#cba6f7">while</span> <span style="color:#89dceb">read</span> -r path; <span style="color:#cba6f7">do</span> 
</span></span><span style="display:flex;"><span>  <span style="color:#f5e0dc">name</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>basename <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">path</span>%.*<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span><span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#cba6f7">if</span> <span style="color:#89dceb;font-weight:bold">[[</span> <span style="color:#cba6f7">$(</span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">result</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | jq -r <span style="color:#a6e3a1">&#34;.ok&#34;</span><span style="color:#cba6f7">)</span> <span style="color:#89dceb;font-weight:bold">==</span> <span style="color:#89dceb">false</span> <span style="color:#89dceb;font-weight:bold">]]</span>; <span style="color:#cba6f7">then</span>
</span></span><span style="display:flex;"><span>  <span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">result</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | jq -r <span style="color:#a6e3a1">&#34;.error&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#89dceb">exit</span> <span style="color:#fab387">1</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">#!/usr/bin/env bash
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb">set</span> -eo pipefail
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>: <span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_COOKIE</span>:=<span style="color:#a6e3a1">}</span>
</span></span><span style="display:flex;"><span>: <span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_TOKEN</span>:=<span style="color:#a6e3a1">}</span>
</span></span><span style="display:flex;"><span>: <span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_DOMAIN</span>:=<span style="color:#a6e3a1">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f5e0dc">files</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>find . -iname <span style="color:#89b4fa">\*</span>.gif -o -iname <span style="color:#89b4fa">\*</span>.png -o -iname <span style="color:#89b4fa">\*</span>.png -maxdepth 1<span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb;font-weight:bold">[[</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">files</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#89dceb;font-weight:bold">==</span> <span style="color:#a6e3a1">&#34;&#34;</span> <span style="color:#89dceb;font-weight:bold">]]</span> <span style="color:#89dceb;font-weight:bold">&amp;&amp;</span> <span style="color:#89dceb">exit</span> <span style="color:#fab387">1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">files</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | <span style="color:#cba6f7">while</span> <span style="color:#89dceb">read</span> -r path; <span style="color:#cba6f7">do</span> 
</span></span><span style="display:flex;"><span>  <span style="color:#f5e0dc">name</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>basename <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">path</span>%.*<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span><span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f5e0dc">result</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>
</span></span><span style="display:flex;"><span>    curl -s --compressed <span style="color:#a6e3a1">&#34;https://</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_DOMAIN</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">.slack.com/api/emoji.add&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>      -H <span style="color:#a6e3a1">&#39;content-type: multipart/form-data&#39;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>      -H <span style="color:#a6e3a1">&#34;cookie: d=</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_COOKIE</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">;&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>      -F <span style="color:#a6e3a1">&#34;token=</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_TOKEN</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>      -F <span style="color:#a6e3a1">&#34;name=</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">name</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>      -F <span style="color:#f5e0dc">mode</span><span style="color:#89dceb;font-weight:bold">=</span>data <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>      -F <span style="color:#a6e3a1">&#34;image=@</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">path</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> 
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">if</span> <span style="color:#89dceb;font-weight:bold">[[</span> <span style="color:#cba6f7">$(</span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">result</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | jq -r <span style="color:#a6e3a1">&#34;.ok&#34;</span><span style="color:#cba6f7">)</span> <span style="color:#89dceb;font-weight:bold">==</span> <span style="color:#89dceb">false</span> <span style="color:#89dceb;font-weight:bold">]]</span>; <span style="color:#cba6f7">then</span>
</span></span><span style="display:flex;"><span>    <span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">result</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | jq -r <span style="color:#a6e3a1">&#34;.error&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#89dceb">exit</span> <span style="color:#fab387">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;uploaded :</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">name</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">:!&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ <span style="color:#f5e0dc">SLACK_DOMAIN</span><span style="color:#89dceb;font-weight:bold">=</span>*** <span style="color:#f5e0dc">SLACK_COOKIE</span><span style="color:#89dceb;font-weight:bold">=</span>*** <span style="color:#f5e0dc">SLACK_TOKEN</span><span style="color:#89dceb;font-weight:bold">=</span>*** ./upload.sh
</span></span><span style="display:flex;"><span>uploaded :stonks:!
</span></span><span style="display:flex;"><span>uploaded :boop:!
</span></span><span style="display:flex;"><span>uploaded :derp:!
</span></span><span style="display:flex;"><span>uploaded :booyah:!
</span></span><span style="display:flex;"><span>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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Once Again, Something Better!</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-"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>The Final Piece &hellip;</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/liberating-custom-slack-emojis/</guid>
      <category>Random Bullshit</category>
      <category>emoji</category>
      <category>api</category>
      <category>bash</category>
      <description>&lt;p&gt;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.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Some Investigative Work</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>	<span style="color:#cba6f7">&#34;ok&#34;</span>: <span style="color:#fab387">true</span>,
</span></span><span style="display:flex;"><span>	<span style="color:#cba6f7">&#34;next_marker&#34;</span>: <span style="color:#a6e3a1">&#34;a-custom-emoji&#34;</span>,
</span></span><span style="display:flex;"><span>	<span style="color:#cba6f7">&#34;results&#34;</span>: [
</span></span><span style="display:flex;"><span>		{
</span></span><span style="display:flex;"><span>			<span style="color:#cba6f7">&#34;name&#34;</span>: <span style="color:#a6e3a1">&#34;another-custom-emoji&#34;</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#cba6f7">&#34;value&#34;</span>: <span style="color:#a6e3a1">&#34;https://emoji.slack-edge.com/T0XXXX/another-custom-emoji/xxxxxxx.png&#34;</span>
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>		<span style="color:#f38ba8">...</span> <span style="color:#f38ba8">more</span> <span style="color:#f38ba8">emojis</span> <span style="color:#f38ba8">...</span>
</span></span><span style="display:flex;"><span>	]
</span></span><span style="display:flex;"><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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>	<span style="color:#cba6f7">&#34;token&#34;</span>: <span style="color:#a6e3a1">&#34;xoxc-xxxx-xxxx-xxxx&#34;</span>,
</span></span><span style="display:flex;"><span>	<span style="color:#cba6f7">&#34;count&#34;</span>: <span style="color:#fab387">100</span>,
</span></span><span style="display:flex;"><span>	<span style="color:#cba6f7">&#34;marker&#34;</span>: <span style="color:#a6e3a1">&#34;some-other-emoji&#34;</span>
</span></span><span style="display:flex;"><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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Testing Your Findings</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ curl --silent https://edgeapi.slack.com/cache/&lt;team&gt;/emojis/list?fp<span style="color:#89dceb;font-weight:bold">=</span><span style="color:#fab387">97</span>
</span></span><span style="display:flex;"><span>  -H <span style="color:#a6e3a1">&#39;cookie: d=&lt;cookie&gt;;&#39;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>  --data-raw <span style="color:#a6e3a1">&#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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ &lt;previous_curl_command&gt; | jq -r <span style="color:#a6e3a1">&#39;.results[].value&#39;</span>
</span></span><span style="display:flex;"><span>https://emoji.slack-edge.com/T0XXXX/no/060fca9aa2581a93.png
</span></span><span style="display:flex;"><span>https://emoji.slack-edge.com/T0XXXX/nods/c7fe54342ab5b8b6.gif
</span></span><span style="display:flex;"><span>https://emoji.slack-edge.com/T0XXXX/nods-back/fbd1b5384cdd0905.gif
</span></span><span style="display:flex;"><span>https://emoji.slack-edge.com/T0XXXX/nomnomnom/612cfc74c785010d.gif
</span></span><span style="display:flex;"><span>https://emoji.slack-edge.com/T0XXXX/octocat/627964d7c9.png
</span></span><span style="display:flex;"><span>https://emoji.slack-edge.com/T0XXXX/ohyou/6a352df984d9c076.gif
</span></span><span style="display:flex;"><span>https://emoji.slack-edge.com/T0XXXX/one-sec-cooking/e749627cb088859d.png
</span></span><span style="display:flex;"><span>https://emoji.slack-edge.com/T0XXXX/oof/db94710445cbb206.png
</span></span><span style="display:flex;"><span>https://emoji.slack-edge.com/T0XXXX/petrol/35ed3db238f795fc.png
</span></span><span style="display:flex;"><span>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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>The Implementation</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>: <span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_WORKSPACE_ID</span>:=<span style="color:#a6e3a1">}</span>
</span></span><span style="display:flex;"><span>: <span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_COOKIE</span>:=<span style="color:#a6e3a1">}</span>
</span></span><span style="display:flex;"><span>: <span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_TOKEN</span>:=<span style="color:#a6e3a1">}</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f5e0dc">payload</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>
</span></span><span style="display:flex;"><span>  jq -nc <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    --arg     token <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_TOKEN</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    --argjson count <span style="color:#a6e3a1">&#34;100&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e3a1">&#39;{
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">      token: $token, 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">      count: $count
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    }&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">)</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#cba6f7">while</span> true; <span style="color:#cba6f7">do</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f5e0dc">result</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>
</span></span><span style="display:flex;"><span>    curl -s --compressed <span style="color:#a6e3a1">&#34;https://edgeapi.slack.com/cache/</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_WORKSPACE_ID</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">/emojis/list?fp=97&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>      -H <span style="color:#a6e3a1">&#34;cookie: d=</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_COOKIE</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">;&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>      --data-raw <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">payload</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> 
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#cba6f7">if</span> <span style="color:#89dceb;font-weight:bold">[[</span> <span style="color:#cba6f7">$(</span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">result</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | jq -r <span style="color:#a6e3a1">&#34;.ok&#34;</span><span style="color:#cba6f7">)</span> <span style="color:#89dceb;font-weight:bold">==</span> <span style="color:#89dceb">false</span> <span style="color:#89dceb;font-weight:bold">]]</span>; <span style="color:#cba6f7">then</span>
</span></span><span style="display:flex;"><span>  <span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">result</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | jq -r <span style="color:#a6e3a1">&#34;.error&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#89dceb">exit</span> <span style="color:#fab387">1</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">result</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span>  | jq -r <span style="color:#a6e3a1">&#39;.results[].value&#39;</span> | <span style="color:#cba6f7">while</span> <span style="color:#89dceb">read</span> -r url; <span style="color:#cba6f7">do</span> 
</span></span><span style="display:flex;"><span><span style="color:#f5e0dc">name</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>basename <span style="color:#cba6f7">$(</span>dirname <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">url</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span><span style="color:#cba6f7">))</span>
</span></span><span style="display:flex;"><span>curl -s -o <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">name</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">.</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">url</span>##*.<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">url</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f5e0dc">marker</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">result</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | jq -r <span style="color:#a6e3a1">&#34;.next_marker&#34;</span><span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb;font-weight:bold">[[</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">marker</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#89dceb;font-weight:bold">==</span> <span style="color:#a6e3a1">&#34;null&#34;</span> <span style="color:#89dceb;font-weight:bold">]]</span> <span style="color:#89dceb;font-weight:bold">&amp;&amp;</span> <span style="color:#89dceb">exit</span> <span style="color:#fab387">0</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f5e0dc">payload</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">payload</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | jq --arg marker <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">marker</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#a6e3a1">&#39;. + { marker: $marker }&#39;</span><span style="color:#cba6f7">)</span>
</span></span></code></pre></div><h2 id="the-finished-product">
  <a class="heading-link" href="#the-finished-product"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>The Finished Product</a>
</h2>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">#!/usr/bin/env bash
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb">set</span> -eo pipefail
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>: <span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_WORKSPACE_ID</span>:=<span style="color:#a6e3a1">}</span>
</span></span><span style="display:flex;"><span>: <span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_COOKIE</span>:=<span style="color:#a6e3a1">}</span>
</span></span><span style="display:flex;"><span>: <span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_TOKEN</span>:=<span style="color:#a6e3a1">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f5e0dc">payload</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>
</span></span><span style="display:flex;"><span>  jq -nc <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    --arg     token <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_TOKEN</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    --argjson count <span style="color:#a6e3a1">&#34;100&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#a6e3a1">&#39;{
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">      token: $token, 
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">      count: $count
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">    }&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">while</span> true; <span style="color:#cba6f7">do</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f5e0dc">result</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>
</span></span><span style="display:flex;"><span>    curl -s --compressed <span style="color:#a6e3a1">&#34;https://edgeapi.slack.com/cache/</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_WORKSPACE_ID</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">/emojis/list?fp=97&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>      -H <span style="color:#a6e3a1">&#34;cookie: d=</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">SLACK_COOKIE</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">;&#34;</span> <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>      --data-raw <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">payload</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> 
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">if</span> <span style="color:#89dceb;font-weight:bold">[[</span> <span style="color:#cba6f7">$(</span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">result</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | jq -r <span style="color:#a6e3a1">&#34;.ok&#34;</span><span style="color:#cba6f7">)</span> <span style="color:#89dceb;font-weight:bold">==</span> <span style="color:#89dceb">false</span> <span style="color:#89dceb;font-weight:bold">]]</span>; <span style="color:#cba6f7">then</span>
</span></span><span style="display:flex;"><span>    <span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">result</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | jq -r <span style="color:#a6e3a1">&#34;.error&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#89dceb">exit</span> <span style="color:#fab387">1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">result</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span>  | jq -r <span style="color:#a6e3a1">&#39;.results[].value&#39;</span> | <span style="color:#cba6f7">while</span> <span style="color:#89dceb">read</span> -r url; <span style="color:#cba6f7">do</span> 
</span></span><span style="display:flex;"><span>    <span style="color:#f5e0dc">name</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>basename <span style="color:#cba6f7">$(</span>dirname <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">url</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span><span style="color:#cba6f7">))</span>
</span></span><span style="display:flex;"><span>    curl -s -o <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">name</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">.</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">url</span>##*.<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">url</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">done</span> 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f5e0dc">marker</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">result</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | jq -r <span style="color:#a6e3a1">&#34;.next_marker&#34;</span><span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#89dceb;font-weight:bold">[[</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">marker</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#89dceb;font-weight:bold">==</span> <span style="color:#a6e3a1">&#34;null&#34;</span> <span style="color:#89dceb;font-weight:bold">]]</span> <span style="color:#89dceb;font-weight:bold">&amp;&amp;</span> <span style="color:#89dceb">exit</span> <span style="color:#fab387">0</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f5e0dc">payload</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">payload</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> | jq --arg marker <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">marker</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#a6e3a1">&#39;. + { marker: $marker }&#39;</span><span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ <span style="color:#f5e0dc">SLACK_WORKSPACE_ID</span><span style="color:#89dceb;font-weight:bold">=</span>*** <span style="color:#f5e0dc">SLACK_COOKIE</span><span style="color:#89dceb;font-weight:bold">=</span>*** <span style="color:#f5e0dc">SLACK_TOKEN</span><span style="color:#89dceb;font-weight:bold">=</span>*** ./fetch.sh
</span></span></code></pre></div><h2 id="an-even-better-version">
  <a class="heading-link" href="#an-even-better-version"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>An Even Better Version!</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>The Bitter Irony</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Bonus Relevant XKCD Image</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/save-money-by-keeping-your-aws-account-clean/</guid>
      <category>Platform Engineering</category>
      <category>aws</category>
      <category>devops</category>
      <category>security</category>
      <category>cost-optimisation</category>
      <description>&lt;p&gt;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.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Enter AWS Nuke</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Added Security</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Soft-Dollar Cost</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Ok, Let&rsquo;s Nuke Some Stuff</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#89dceb">export</span> <span style="color:#f5e0dc">AWS_ACCESS_KEY_ID</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">&#34;***&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb">export</span> <span style="color:#f5e0dc">AWS_SECRET_ACCESS_KEY</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">&#34;***&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb">export</span> <span style="color:#f5e0dc">AWS_SESSION_TOKEN</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">&#34;***&#34;</span>
</span></span><span style="display:flex;"><span>aws sts get-caller-identity
</span></span><span style="display:flex;"><span><span style="color:#89dceb;font-weight:bold">{</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e3a1">&#34;UserId&#34;</span>: <span style="color:#a6e3a1">&#34;XXXXXXXXXXXXXXXX:DEADBEEF&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e3a1">&#34;Account&#34;</span>: <span style="color:#a6e3a1">&#34;11111111111&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e3a1">&#34;Arn&#34;</span>: <span style="color:#a6e3a1">&#34;arn:aws:sts::11111111111:assumed-role/AWSReservedSSO_AdministratorAccess/DEADBEEF&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb;font-weight:bold">}</span>
</span></span></code></pre></div><h2 id="configuration">
  <a class="heading-link" href="#configuration"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Configuration</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#fab387">---</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">regions</span>:
</span></span><span style="display:flex;"><span>  - global
</span></span><span style="display:flex;"><span>  - eu-north-1
</span></span><span style="display:flex;"><span>  - ap-south-1
</span></span><span style="display:flex;"><span>  - eu-west-3
</span></span><span style="display:flex;"><span>  - eu-west-2
</span></span><span style="display:flex;"><span>  - eu-west-1
</span></span><span style="display:flex;"><span>  - ap-northeast-3
</span></span><span style="display:flex;"><span>  - ap-northeast-2
</span></span><span style="display:flex;"><span>  - ap-northeast-1
</span></span><span style="display:flex;"><span>  - sa-east-1
</span></span><span style="display:flex;"><span>  - ca-central-1
</span></span><span style="display:flex;"><span>  - ap-southeast-1
</span></span><span style="display:flex;"><span>  - ap-southeast-2
</span></span><span style="display:flex;"><span>  - eu-central-1
</span></span><span style="display:flex;"><span>  - us-east-1
</span></span><span style="display:flex;"><span>  - us-east-2
</span></span><span style="display:flex;"><span>  - us-west-1
</span></span><span style="display:flex;"><span>  - us-west-2
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">account-blocklist</span>:
</span></span><span style="display:flex;"><span>- <span style="color:#fab387">12345678910</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">resource-types</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">excludes</span>:
</span></span><span style="display:flex;"><span>    - CloudWatchAlarm
</span></span><span style="display:flex;"><span>    - Route53ResolverRule
</span></span><span style="display:flex;"><span>    - Route53HostedZone
</span></span><span style="display:flex;"><span>    - S3Object
</span></span><span style="display:flex;"><span>    - S3Bucket
</span></span><span style="display:flex;"><span>    - GuardDutyDetector
</span></span><span style="display:flex;"><span>    - SNSSubscription
</span></span><span style="display:flex;"><span>    - IAMSAMLProvider
</span></span><span style="display:flex;"><span>    - CloudTrailTrail
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">accounts</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">11111111111</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">filters</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#cba6f7">IAMPolicy</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#cba6f7">type</span>: contains
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">value</span>: AWS-Chatbot-NotificationsOnly-Policy
</span></span><span style="display:flex;"><span>      <span style="color:#cba6f7">IAMRolePolicy</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#cba6f7">type</span>: contains
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">value</span>: CloudTrailCloudWatchLogsRole
</span></span><span style="display:flex;"><span>        - <span style="color:#cba6f7">type</span>: contains
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">value</span>: OrganizationAccountAccessRole
</span></span><span style="display:flex;"><span>      <span style="color:#cba6f7">IAMRole</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#cba6f7">type</span>: contains
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">value</span>: AWSReservedSSO
</span></span><span style="display:flex;"><span>        - <span style="color:#cba6f7">type</span>: contains
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">value</span>: CloudWatchAlarmToSlackRole
</span></span><span style="display:flex;"><span>        - CloudTrailCloudWatchLogsRole
</span></span><span style="display:flex;"><span>        - OrganizationAccountAccessRole
</span></span><span style="display:flex;"><span>      <span style="color:#cba6f7">IAMRolePolicyAttachment</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#cba6f7">type</span>: contains
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">value</span>: AWSReservedSSO
</span></span><span style="display:flex;"><span>        - <span style="color:#cba6f7">type</span>: contains
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">value</span>: CloudWatchAlarmToSlackRole
</span></span><span style="display:flex;"><span>      <span style="color:#cba6f7">SNSTopic</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#cba6f7">type</span>: contains
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">value</span>: CloudWatchAlarms
</span></span><span style="display:flex;"><span>      <span style="color:#cba6f7">CloudWatchLogsLogGroup</span>:
</span></span><span style="display:flex;"><span>        - CloudTrail/DefaultLogGroup
</span></span></code></pre></div><h4 id="regions">
  <a class="heading-link" href="#regions"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span><code>regions</code></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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span><code>account-blocklist</code></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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span><code>resource-types.excludes</code></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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span><code>accounts</code></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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Using the Tool</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker run --rm -it <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -v ~/aws-nuke-config.yml:/home/aws-nuke/config.yml <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -e AWS_ACCESS_KEY_ID <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -e AWS_SECRET_ACCESS_KEY <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    -e AWS_SESSION_TOKEN <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    quay.io/rebuy/aws-nuke:main <span style="color:#89b4fa">\
</span></span></span><span style="display:flex;"><span>    --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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>aws-nuke version v2.19.0.15.gb46fbe0 - Wed Oct  5 10:01:13 UTC 2022 - b46fbe0e9f63266f56b5afd9635b4e4d5a3108d4
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Do you really want to nuke the account with the ID 11111111111 and the alias &#39;aws-nuke-account&#39;?
</span></span><span style="display:flex;"><span>Do you want to continue? Enter account alias to continue.
</span></span><span style="display:flex;"><span>&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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>global - IAMRole - RDSBucketAccessRole - [Name: &#34;RDSBucketAccessRole&#34;, Path: &#34;/service-role/&#34;] - filtered by config
</span></span><span style="display:flex;"><span>global - IAMRole - S3BucketAccessRole - [Name: &#34;S3BucketAccessRole&#34;, Path: &#34;/&#34;] - would remove
</span></span><span style="display:flex;"><span>global - IAMRole - EBSSnapshotReplicationRole - [Name: &#34;EBSSnapshotReplicationRole&#34;, Path: &#34;/&#34;] - would remove
</span></span><span style="display:flex;"><span>global - IAMRole - VPCFlowLogsRole - [Name: &#34;VPCFlowLogsRole&#34;, Path: &#34;/&#34;] - would remove
</span></span><span style="display:flex;"><span>global - IAMRole - OrganizationAccountAccessRole - [Name: &#34;OrganizationAccountAccessRole&#34;, Path: &#34;/&#34;] - filtered by config
</span></span><span style="display:flex;"><span>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 style="display:flex;"><span>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 style="display:flex;"><span>Scan complete: 161 total, 83 nukeable, 78 filtered.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>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-"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>In Conclusion &hellip;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>100% Completion Speed-Run</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/deleting-massive-s3-buckets-the-easy-way/</guid>
      <category>Platform Engineering</category>
      <category>aws</category>
      <category>devops</category>
      <category>cost-optimisation</category>
      <description>&lt;p&gt;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.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>What&rsquo;s the problem?</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Lifecycle Policies!</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Expire those objects!</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Delete those objects!</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>And now&hellip; we wait.</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>And that&rsquo;s that!</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Several days later&hellip;</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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/messing-around-with-txt-records/</guid>
      <category>Random Bullshit</category>
      <category>dns</category>
      <category>devops</category>
      <category>networking</category>
      <description>&lt;p&gt;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.&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Wanna see a neat trick?</a>
</h2>
<p>Open up your terminal of choice and invoke this <code>dig</code> command:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>&#34;twitter: @wilhelm&#34;
</span></span><span style="display:flex;"><span>&#34;github:  wilhelm-murdoch&#34;
</span></span><span style="display:flex;"><span>&#34;keybase: 7F89 5036 2816 06F6&#34;
</span></span><span style="display:flex;"><span>&#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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>What you&rsquo;re looking at.</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Why might you do this?</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Here&rsquo;s you would implement it.</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>That&rsquo;s it!</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 and 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 (ɯlǝɥlᴉʍ)</author>
      <guid>https://wilhelm.codes/blog/falsifying-github-participation-graphs-for-fun-and-profit/</guid>
      <category>Random Bullshit</category>
      <category>bash</category>
      <category>git</category>
      <description>&lt;p&gt;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;&lt;/p&gt;</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>What&rsquo;s your point?</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>What to do?</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Ok, show me.</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Configure Your Participation Graph</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>New Private Repository</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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Checkout Local Working Copy</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#89dceb">echo</span> <span style="color:#a6e3a1">&#34;Let&#39;s goooooooo!&#34;</span> &gt;&gt; README.md
</span></span><span style="display:flex;"><span>git init
</span></span><span style="display:flex;"><span>git add README.md
</span></span><span style="display:flex;"><span>git commit -m <span style="color:#a6e3a1">&#34;The first and final legitimate commit in this repository ...&#34;</span>
</span></span><span style="display:flex;"><span>git branch -M main
</span></span><span style="display:flex;"><span>git remote add origin git@github.com:&lt;account&gt;/github-participation.git
</span></span><span style="display:flex;"><span>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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Writing Some Code</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f5e0dc">start</span><span style="color:#89dceb;font-weight:bold">=</span>2021-05-01
</span></span><span style="display:flex;"><span><span style="color:#f5e0dc">stop</span><span style="color:#89dceb;font-weight:bold">=</span>2021-05-09
</span></span><span style="display:flex;"><span><span style="color:#f5e0dc">max</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#fab387">20</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">while</span> <span style="color:#89dceb;font-weight:bold">[[</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">start</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> !<span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">stop</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span> <span style="color:#89dceb;font-weight:bold">]]</span>; <span style="color:#cba6f7">do</span> 
</span></span><span style="display:flex;"><span>  <span style="color:#f5e0dc">start</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#cba6f7">$(</span>gdate -I -d <span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">start</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1"> + 1 day&#34;</span><span style="color:#cba6f7">)</span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">for</span> <span style="color:#89dceb;font-weight:bold">((</span><span style="color:#f5e0dc">run</span><span style="color:#89dceb;font-weight:bold">=</span>1; run &lt;<span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">$((</span>RANDOM <span style="color:#89dceb;font-weight:bold">%</span> max <span style="color:#89dceb;font-weight:bold">+</span> <span style="color:#fab387">1</span><span style="color:#cba6f7">))</span>; run++<span style="color:#89dceb;font-weight:bold">))</span>; <span style="color:#cba6f7">do</span>
</span></span><span style="display:flex;"><span>    openssl rand -hex <span style="color:#fab387">32</span> &gt; touch.txt
</span></span><span style="display:flex;"><span>    git add -A
</span></span><span style="display:flex;"><span>    <span style="color:#89dceb">export</span> <span style="color:#f5e0dc">GIT_AUTHOR_DATE</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">start</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">T00:01&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#89dceb">export</span> <span style="color:#f5e0dc">GIT_COMMITTER_DATE</span><span style="color:#89dceb;font-weight:bold">=</span><span style="color:#a6e3a1">&#34;</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">GIT_AUTHOR_DATE</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span>
</span></span><span style="display:flex;"><span>    git commit -m <span style="color:#a6e3a1">&#34;Touch #</span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">run</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1"> for </span><span style="color:#a6e3a1">${</span><span style="color:#f5e0dc">GIT_AUTHOR_DATE</span><span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">done</span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#89dceb;font-weight:bold">[</span>main 2ebb5b9<span style="color:#89dceb;font-weight:bold">]</span> Touch <span style="color:#6c7086;font-style:italic">#1 2021-05-07T00:01</span>
</span></span><span style="display:flex;"><span> <span style="color:#fab387">1</span> file changed, <span style="color:#fab387">1</span> insertion<span style="color:#89dceb;font-weight:bold">(</span>+<span style="color:#89dceb;font-weight:bold">)</span>, <span style="color:#fab387">1</span> deletion<span style="color:#89dceb;font-weight:bold">(</span>-<span style="color:#89dceb;font-weight:bold">)</span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb;font-weight:bold">[</span>main e104b0a<span style="color:#89dceb;font-weight:bold">]</span> Touch <span style="color:#6c7086;font-style:italic">#2 2021-05-07T00:01</span>
</span></span><span style="display:flex;"><span> <span style="color:#fab387">1</span> file changed, <span style="color:#fab387">1</span> insertion<span style="color:#89dceb;font-weight:bold">(</span>+<span style="color:#89dceb;font-weight:bold">)</span>, <span style="color:#fab387">1</span> deletion<span style="color:#89dceb;font-weight:bold">(</span>-<span style="color:#89dceb;font-weight:bold">)</span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb;font-weight:bold">[</span>main 873fa39<span style="color:#89dceb;font-weight:bold">]</span> Touch <span style="color:#6c7086;font-style:italic">#3 2021-05-07T00:01</span>
</span></span><span style="display:flex;"><span> <span style="color:#fab387">1</span> file changed, <span style="color:#fab387">1</span> insertion<span style="color:#89dceb;font-weight:bold">(</span>+<span style="color:#89dceb;font-weight:bold">)</span>, <span style="color:#fab387">1</span> deletion<span style="color:#89dceb;font-weight:bold">(</span>-<span style="color:#89dceb;font-weight:bold">)</span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb;font-weight:bold">[</span>main 9def564<span style="color:#89dceb;font-weight:bold">]</span> Touch <span style="color:#6c7086;font-style:italic">#4 2021-05-07T00:01</span>
</span></span><span style="display:flex;"><span> <span style="color:#fab387">1</span> file changed, <span style="color:#fab387">1</span> insertion<span style="color:#89dceb;font-weight:bold">(</span>+<span style="color:#89dceb;font-weight:bold">)</span>, <span style="color:#fab387">1</span> deletion<span style="color:#89dceb;font-weight:bold">(</span>-<span style="color:#89dceb;font-weight:bold">)</span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb;font-weight:bold">[</span>main 1b83187<span style="color:#89dceb;font-weight:bold">]</span> Touch <span style="color:#6c7086;font-style:italic">#1 2021-05-08T00:01</span>
</span></span><span style="display:flex;"><span> <span style="color:#fab387">1</span> file changed, <span style="color:#fab387">1</span> insertion<span style="color:#89dceb;font-weight:bold">(</span>+<span style="color:#89dceb;font-weight:bold">)</span>, <span style="color:#fab387">1</span> deletion<span style="color:#89dceb;font-weight:bold">(</span>-<span style="color:#89dceb;font-weight:bold">)</span>
</span></span><span style="display:flex;"><span><span style="color:#89dceb;font-weight:bold">[</span>main b9f00db<span style="color:#89dceb;font-weight:bold">]</span> Touch <span style="color:#6c7086;font-style:italic">#2 2021-05-08T00:01</span>
</span></span><span style="display:flex;"><span> <span style="color:#fab387">1</span> file changed, <span style="color:#fab387">1</span> insertion<span style="color:#89dceb;font-weight:bold">(</span>+<span style="color:#89dceb;font-weight:bold">)</span>, <span style="color:#fab387">1</span> deletion<span style="color:#89dceb;font-weight:bold">(</span>-<span style="color:#89dceb;font-weight:bold">)</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" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>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"><span class="heading-anchor" aria-hidden="true">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
        focusable="false"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
      </svg>
    </span>Any final thoughts?</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>
