<?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/" xmlns:wc="https://wilhelm.codes/ns/feed/1.0"><channel><title>Wilhelm Codes · Github</title><link>https://wilhelm.codes/tags/github/</link><description>Slinger of nibbles, bits and bytes. Over 25 years of professional experience as a software engineer. Love making glowing rectangles go "beep-boop".</description><generator>Hugo</generator><language>en-US</language><managingEditor>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</managingEditor><webMaster>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</webMaster><lastBuildDate>Mon, 24 Aug 2026 11:00:00 +1000</lastBuildDate><atom:link href="https://wilhelm.codes/tags/github/index.xml" rel="self" type="application/rss+xml"/><item><title>Welp, I finally did it! I’ve moved the git repository for this site …</title><link>https://wilhelm.codes/bits/update-adios-github/</link><pubDate>Mon, 24 Aug 2026 11:00:00 +1000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/bits/update-adios-github/</guid><category>wilhelm.codes</category><category>github</category><wc:kind>bit</wc:kind><wc:bit>update</wc:bit><wc:untitled>true</wc:untitled><description>Welp, I finally did it! I&amp;rsquo;ve moved the git repository for this site from Github to the Forgejo instance in my homelab. Further, I&amp;rsquo;ve replaced Cloudflare&amp;rsquo;s build pipeline with a simple 20-line YAML file used by my Woodpecker CI server, which is also hosted in my homelab. I&amp;rsquo;m still using Wrangler to push the static assets to Cloudflare Pages and I&amp;rsquo;ll always need an external DNS provider for my domain, but you can&amp;rsquo;t reasonably self-host everything.</description><content:encoded><![CDATA[<p>Welp, I finally did it! I&rsquo;ve moved the git repository for this site from Github to the Forgejo instance in my homelab. Further, I&rsquo;ve replaced Cloudflare&rsquo;s build pipeline with a simple 20-line YAML file used by my Woodpecker CI server, which is also hosted in my homelab. I&rsquo;m still using Wrangler to push the static assets to Cloudflare Pages and I&rsquo;ll always need an external DNS provider for my domain, but you can&rsquo;t reasonably self-host <em>everything</em>.</p>
<p>It&rsquo;s nice to have <em>almost</em> full control of my little website now! And all it took was 3 attempts to get the integration right. 😅</p>
<p><img
  src="https://wilhelm.codes/bits/update-adios-github/changelog-proof_hu_6623937c7992b7c5.webp"
  srcset="/bits/update-adios-github/changelog-proof_hu_6623937c7992b7c5.webp 736w, /bits/update-adios-github/changelog-proof_hu_e96734be3e965506.webp 1104w, /bits/update-adios-github/changelog-proof_hu_375583fd984255af.webp 1472w"
  sizes="(max-width: 48rem) 100vw, 736px"
  width="736"
  height="273"
  alt="Alternative text for screen readers"
  
  loading="lazy"
  decoding="async"
/>
</p>
<p>A great note to end on for a Monday evening!</p>
]]></content:encoded></item><item><title>I am very smart. I only just realised over the weekend that issues and …</title><link>https://wilhelm.codes/bits/update-forgot-to-turn-on-comments/</link><pubDate>Mon, 29 Jun 2026 11:47:27 +1000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/bits/update-forgot-to-turn-on-comments/</guid><category>wilhelm.codes</category><category>github</category><wc:kind>bit</wc:kind><wc:bit>update</wc:bit><wc:untitled>true</wc:untitled><description>I am very smart. I only just realised over the weekend that issues and discussions in the parent repo for this site were disabled. Meaning, article coments using Giscuss was functionally disabled here. WOOPS!</description><content:encoded>&lt;p>I am very smart. I only just realised over the weekend that issues and discussions in the parent repo for this site were disabled. Meaning, article coments using Giscuss was functionally disabled &lt;em>here&lt;/em>. WOOPS!&lt;/p>
</content:encoded></item><item><title>A Changelog That Builds Itself</title><link>https://wilhelm.codes/blog/a-changelog-that-builds-itself/</link><pubDate>Sun, 14 Jun 2026 00:00:00 +0000</pubDate><author>0xdeadbeef@devilmayco.de (Wilhelm Murdoch)</author><guid>https://wilhelm.codes/blog/a-changelog-that-builds-itself/</guid><category>hugo</category><category>github</category><category>api</category><wc:kind>post</wc:kind><description>This site has a changelog page. It&amp;rsquo;s not one I write by hand as it builds itself from my activity on GitHub every time the site deploys. I think it&amp;rsquo;s a neat little trick. It also spent a solid afternoon teaching me that &amp;ldquo;works on my machine&amp;rdquo; and &amp;ldquo;works in production&amp;rdquo; are, once again, two very different things.</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="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">url</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">&#34;https://api.github.com/repos/wilhelm-murdoch/wilhelm.codes/events&#34;</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nf">try</span><span class="w"> </span><span class="p">(</span><span class="nx">resources</span><span class="p">.</span><span class="nx">GetRemote</span><span class="w"> </span><span class="err">$</span><span class="nx">url</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">events</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">.</span><span class="nx">Value</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">transform</span><span class="p">.</span><span class="nx">Unmarshal</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="cm">/* ...range over them and render... */</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>Because this all happens at <em>build</em> time, the visitor never waits on GitHub. By the time the page reaches a browser it&rsquo;s just static HTML like everything else. The changelog is always current as of the last deploy, and I never have to think about it.</p>
<p>That&rsquo;s the part that worked. Now for the parts that didn&rsquo;t.</p>
<h2 id="-then-production-happened">
  <a class="heading-link" href="#-then-production-happened">&hellip; Then, production happened.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>I&rsquo;ve been more or less absent from this site for close to a year, but this past week I&rsquo;ve decided to breathe a bit more life into it. I made a few changes to <a href="https://wilhelm.codes/blog/some-long-overdue-housekeeping/">modernise</a> the stack, pushed my changes up and waited on Cloudflare to do its thing and&hellip;</p>
<div class="code-block">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">ERROR error building site: ... error calling GetRemote:
</span></span><span class="line"><span class="cl">failed to fetch remote resource from &#39;https://api.github.com/...&#39;: Forbidden</span></span></code></pre></div>
</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="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">dict</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">Getenv</span><span class="w"> </span><span class="s">&#34;HUGO_GITHUB_TOKEN&#34;</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;headers&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">dict</span><span class="w"> </span><span class="s">&#34;Authorization&#34;</span><span class="w"> </span><span class="p">(</span><span class="nx">printf</span><span class="w"> </span><span class="s">&#34;Bearer %s&#34;</span><span class="w"> </span><span class="p">.))</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">remote</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">try</span><span class="w"> </span><span class="p">(</span><span class="nx">resources</span><span class="p">.</span><span class="nx">GetRemote</span><span class="w"> </span><span class="err">$</span><span class="nx">url</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>It&rsquo;s worth noting that Hugo won&rsquo;t read just <em>any</em> environment variable. Its security policy only lets <code>os.Getenv</code> see variables prefixed with <code>HUGO_</code>. Name your token <code>GITHUB_TOKEN</code> and you&rsquo;ll get a confusing fistful of nothing; name it <code>HUGO_GITHUB_TOKEN</code> and it works. Ask me how I know.</p>
<h2 id="hark-another-footgun">
  <a class="heading-link" href="#hark-another-footgun">Hark! Another Footgun!<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>With the token in place, the fetch succeeded and the build promptly fell over somewhere new:</p>
<div class="code-block">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">error calling len: reflect: call of reflect.Value.Type on zero Value</span></span></code></pre></div>
</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="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nf">eq</span><span class="w"> </span><span class="p">(</span><span class="nx">len</span><span class="w"> </span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">commits</span><span class="p">)</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</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="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="p">.</span><span class="nx">payload</span><span class="p">.</span><span class="nx">commits</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="nf">eq</span><span class="w"> </span><span class="p">(</span><span class="nx">len</span><span class="w"> </span><span class="p">.)</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="cm">/* the one-commit case */</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">{{</span><span class="cm">/* the many-commits case */</span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>Since I&rsquo;ve implemented PAT support, I&rsquo;m not so concerned about being rate-limited during local development, though I now use the fixtures as a fallback if any request fails. This allows me to keep working locally without disruption.</p>
<h2 id="dont-let-someone-elses-server-break-your-build">
  <a class="heading-link" href="#dont-let-someone-elses-server-break-your-build">Don&rsquo;t let someone else&rsquo;s server break your build.<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>The real personal lesson is the moment your build depends on a third party at build time, you&rsquo;ve effectively ceded control of your deployment to them. GitHub rate-limits you, or has a wobble, or changes a payload shape, and suddenly your perfectly good site won&rsquo;t deploy and you&rsquo;ll have a hard time.</p>
<p>So the changelog no longer treats GitHub as load-bearing. If the fetch fails for <em>any</em> reason it shrugs, logs a warning, and falls back to that saved fixture instead of taking the whole deploy down with it:</p>
<div class="code-block">
  <div class="code-head">
    <span class="code-file"></span>
    <span class="code-lang">Go</span>
  </div>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">remote</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">try</span><span class="w"> </span><span class="p">(</span><span class="nx">resources</span><span class="p">.</span><span class="nx">GetRemote</span><span class="w"> </span><span class="err">$</span><span class="nx">url</span><span class="w"> </span><span class="err">$</span><span class="nx">opts</span><span class="p">)</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="err">$</span><span class="nx">remote</span><span class="p">.</span><span class="nx">Err</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="nx">warnf</span><span class="w"> </span><span class="s">&#34;changelog: GitHub fetch failed (%s); using the fixture&#34;</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">response</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">ReadFile</span><span class="w"> </span><span class="s">&#34;static/github.json&#34;</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">transform</span><span class="p">.</span><span class="nx">Unmarshal</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="nx">with</span><span class="w"> </span><span class="err">$</span><span class="nx">remote</span><span class="p">.</span><span class="nx">Value</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{{</span><span class="w"> </span><span class="err">$</span><span class="nx">response</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="nx">transform</span><span class="p">.</span><span class="nx">Unmarshal</span><span class="w"> </span><span class="p">}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="w"> </span><span class="nx">end</span><span class="w"> </span><span class="p">}}</span></span></span></code></pre></div>
</div>
<p>The <code>try</code> keyword is the hero here as it catches the error that <code>GetRemote</code> would otherwise throw and hands it back to me as a value I can actually <em>do</em> something with, rather than a smoking crater where my build used to be. Worst case, the page shows slightly stale history. That&rsquo;s a trade I&rsquo;ll take every single time over a red deploy or disruption to my local flow.</p>
<h2 id="in-conclusion">
  <a class="heading-link" href="#in-conclusion">In conclusion&hellip;<span class="heading-anchor" aria-hidden="true">#</span>
  </a>
</h2>
<p>Ironically, this is something I&rsquo;ve dealt with quite frequently at work. Github &amp; AWS API rate limits ( yes, we frequently got rate limited by the VPC API of all things ), <a href="https://yarnpkg.com/">Yarn</a> will throw a <code>5xx</code> and even Sentry will chimp out during source map uploads. All of these things can disrupt deployments.</p>
<p>The point is just assume these things will happen and then build defensively around them. I, ehhhh, just need to apply this wisdom to my personal stuff. 😅</p>
<p>Anyway, the changelog still builds itself. It just doesn&rsquo;t get to take the rest of the site down with it anymore.</p>]]></content:encoded></item></channel></rss>