<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Read at BYK&apos;s</title><description>Random ramblings of a software engineer. Mostly about software, sometimes about life.</description><link>https://byk.im/rss.xml</link><language>en-us</language><item><title>Adaptation: new tools in town!</title><link>https://byk.im/posts/adaptation-new-tools-in-town</link><guid isPermaLink="true">https://byk.im/posts/adaptation-new-tools-in-town</guid><description>It is not the strongest of the species that survives, nor the most intelligent that survives. It is the one that is most adaptable to change.</description><pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It was about a year ago. I was getting bombarded with LLMs in my editor. It started with &lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;VSCode&lt;/a&gt; offering code snippets as random auto complete suggestions. I hated them. They were mostly dumb, confused me and now I actually had to think deeper before accepting a suggestion because they actually could be subtly broken code. Then, very occasionally but at an increasing frequency, the suggestions started to understand my intent and predict my actual next step. That was oddly satisfying&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. Even worked on my blog posts since I write these in markdown in my editor. Then came the inline ask mode where I asked it to do a very boring and repetitive task. It did it quite well and quite inefficiently. I could have used &lt;a href=&quot;https://ast-grep.github.io/&quot;&gt;ast-grep&lt;/a&gt; for that but, that also costs a lot of brain tokens y’know? &lt;a href=&quot;https://cra.mr&quot;&gt;Cramer&lt;/a&gt; gave us all &lt;a href=&quot;https://cursor.com/&quot;&gt;Cursor&lt;/a&gt; access and encouraged us to try. &lt;a href=&quot;https://syntax.fm&quot;&gt;Syntax podcast&lt;/a&gt; was full of episodes of comparing Cursor to &lt;a href=&quot;https://github.com/features/copilot&quot;&gt;Copilot&lt;/a&gt; to &lt;a href=&quot;https://windsurf.com/&quot;&gt;Windsurf&lt;/a&gt;, my new colleague &lt;a href=&quot;https://x.com/betegon&quot;&gt;Miguel&lt;/a&gt; seemed to be cranking out big patches with AI (they did have some silly stuff in it but overall, they were good) and &lt;em&gt;I&lt;/em&gt; still couldn’t get myself to use it. Don’t get me wrong, I did try many times. The issue was, it felt like trying to work with a thick but self-assured person getting all the core concepts wrong. I kept hearing people talking about generating a plan or a todo list for the agents to follow and I was thinking “The reason I became a software engineer is because I’m lazy. If I’m gonna write down how to do it exactly, I might as well do it myself, and better&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;”.&lt;/p&gt;
&lt;p&gt;Then, during one school break trip came that fateful moment. I was too tired to actually code and the code that needed to be written was quite tedious. I needed to parse &lt;code&gt;stdout&lt;/code&gt; of random processes, fuzzy match them with logs I might have been receiving and do more stuff accordingly.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; But again, I was tired and even if I wasn’t, stdout plumbing with random processes is as fun as a root canal&lt;sup&gt;&lt;a href=&quot;#user-content-fn-4&quot; id=&quot;user-content-fnref-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;. At that point I said “screw it, I won’t be able to get this done and I already planned it out in my head. I can dump it to Cursor and see where it goes with it.” Turns out Cursor added the “plan mode” around the same time. See, you don’t have to &lt;em&gt;write&lt;/em&gt; the plan, the AI can write the plan for you &lt;em&gt;and&lt;/em&gt; execute it too! Believe it or not, it one-shotted the entire patch and probably better than what my first pass would have been. I was excited, dumbstruck, curious, furious, and everything in between. I threw more problems to it and just kept getting good results. I also got a lot of &lt;em&gt;literal&lt;/em&gt; heat though! Don’t know why&lt;sup&gt;&lt;a href=&quot;#user-content-fn-5&quot; id=&quot;user-content-fnref-5&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;5&lt;/a&gt;&lt;/sup&gt; but despite the model running in the cloud, just the agent loop itself was making my laptop sweat&lt;sup&gt;&lt;a href=&quot;#user-content-fn-6&quot; id=&quot;user-content-fnref-6&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;This was only the beginning though. A few months later, on another school break trip&lt;sup&gt;&lt;a href=&quot;#user-content-fn-7&quot; id=&quot;user-content-fnref-7&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;, a wild &lt;del&gt;Pokémon&lt;/del&gt; build in cloud feature appeared! This was a game changer and became my other breaking point. Building in cloud means my computer becomes free after the planning session. Which means the only bottleneck is now the planning phase. And that, my friends, is the actual force multiplier. The real productivity gain that people have been talking about. See, even if the AI does a slightly &lt;em&gt;worse&lt;/em&gt; job than myself, I can now practically work on many things in parallel through delegation. I started to realize there was a hidden PM in me! 😱&lt;/p&gt;
&lt;p&gt;I rapidly started throwing GitHub issues and ideas at it, generating plans and then offloading to cloud build agents. For some silly reason, the planning phase is restricted to the local computer but it’s not a deal breaker. To review the code generated by AI, I decided to use draft PRs on GitHub as it is a much nicer experience than my editor. It also only involves switching tabs rather than Git branches. The only issue is, now I look like a mad man arguing with himself on the PRs, publicly 😅&lt;sup&gt;&lt;a href=&quot;#user-content-fn-8&quot; id=&quot;user-content-fnref-8&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;8&lt;/a&gt;&lt;/sup&gt;. I &lt;a href=&quot;https://github.com/BYK/dotskills/blob/main/iterate-pr.md&quot;&gt;created this “skill”&lt;/a&gt;, telling my agent to fix CI builds and address all review comments until there are none. This was bliss. I even bought a foldable phone&lt;sup&gt;&lt;a href=&quot;#user-content-fn-9&quot; id=&quot;user-content-fnref-9&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;9&lt;/a&gt;&lt;/sup&gt; so I can review these PRs easily on the go and keep the agents busy! I was hooked. I was shipping like crazy and I was checking my phone frequently to see &lt;del&gt;my likes on Instagram&lt;/del&gt; my agents’ progress. And then, just like it came out of nowhere, “build on cloud” went to nowhere. “The Lord giveth, The Lord taketh away” I guess. Back were the dark days of being stuck with a single task and a loud and hot laptop.&lt;/p&gt;
&lt;p&gt;That’s when I decided to build my ultimate setup. But that, friends, is the topic of my next post.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;And creepy. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;With blackjack and hookers. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;This was the first prototype of &lt;code&gt;spotlight run&lt;/code&gt; &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;Ironically, I found it very exciting for the very first time! &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-5&quot;&gt;
&lt;p&gt;Hi Norah Jones. &lt;a href=&quot;#user-content-fnref-5&quot; data-footnote-backref aria-label=&quot;Back to reference 5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-6&quot;&gt;
&lt;p&gt;Well, more like &lt;em&gt;boil&lt;/em&gt;, probably the biggest contributor to my thermal paste sputtering around the die. &lt;a href=&quot;#user-content-fnref-6&quot; data-footnote-backref aria-label=&quot;Back to reference 6&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-7&quot;&gt;
&lt;p&gt;Yes, we have so many school breaks. No, I don’t know exactly why. &lt;a href=&quot;#user-content-fnref-7&quot; data-footnote-backref aria-label=&quot;Back to reference 7&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-8&quot;&gt;
&lt;p&gt;Because the bot uses my own credentials for the PR through &lt;code&gt;gh&lt;/code&gt; &lt;a href=&quot;#user-content-fnref-8&quot; data-footnote-backref aria-label=&quot;Back to reference 8&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-9&quot;&gt;
&lt;p&gt;Pixel 10 Pro Fold. Yup, that’s a mouthful and is quite expensive but was okay with a trade-in with my old phone. &lt;a href=&quot;#user-content-fnref-9&quot; data-footnote-backref aria-label=&quot;Back to reference 9&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item><item><title>Releasing Packages with a Valet Key: npm, PyPI, and beyond</title><link>https://byk.im/posts/releasing-packages</link><guid isPermaLink="true">https://byk.im/posts/releasing-packages</guid><description>How we built a secure, auditable, and low-friction release system at Sentry</description><pubDate>Tue, 25 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Disclaimer: This post should have been written about 5 years ago but I never got around to it; with the most recent &lt;a href=&quot;https://socket.dev/blog/shai-hulud-strikes-again-v2&quot;&gt;Shai-Hulud attack&lt;/a&gt;, I thought it would be a good time to finally check this off the list and hopefully help others avoid supply-chain attacks.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;About 5 years ago, I sat in on a meeting at Sentry in the midst of their SOC 2 compliance efforts. There was &lt;a href=&quot;https://lucumr.pocoo.org/&quot;&gt;Armin&lt;/a&gt;, telling us that we needed a secret storage service for our package repository tokens. The tokens we used to deploy Sentry SDKs to package repositories such as npm, PyPI etc. This was to ensure there were no unauthorized releases of our SDKs which were embedded into all Sentry customers’ products. There were a limited set of people who had access to these tokens back in the time. Now, they became the bottleneck for more and more frequent releases. There was also the auditability issue at hand: releases were performed from individuals’ workstations and there was no easy way to trace a release back to where it originated from or whether it was authorized or not.&lt;/p&gt;
&lt;p&gt;For some reason I intuitively was against such a secret storage service and felt like the answer was somewhere in GitHub, GitHub Actions, and their secret storage service we already used. We already had the repo permissions, personnel structure, and all the visibility for auditing there. Heck, even the approval mechanics were there with pull requests. So I said “give me a week and I’ll get you a proof of concept” which Armin did and I delivered - though I think it took a bit more than a week 😅&lt;/p&gt;
&lt;h2 id=&quot;secrets-in-plain-sight&quot;&gt;Secrets in Plain Sight&lt;/h2&gt;
&lt;p&gt;Before we dive into the solution, let me paint a picture of the problem. Publishing packages to registries like npm, PyPI, or crates.io requires access tokens. These tokens are essentially the keys to the kingdom - whoever has them can publish anything under your organization’s name. At the time, these tokens were either distributed to select individuals, or lived in GitHub repository secrets, accessible to anyone with write access to the repository.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Now, here’s the scary part: at Sentry, we had 90-100+ engineers with commit rights to our SDK repositories. Any one of them could:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new workflow or modify an existing one&lt;/li&gt;
&lt;li&gt;Access these secrets within that workflow&lt;/li&gt;
&lt;li&gt;Exfiltrate them to any web service they controlled&lt;/li&gt;
&lt;li&gt;Do all of the above without triggering any alarms&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And the truly terrifying bit? Even if someone &lt;em&gt;did&lt;/em&gt; steal these tokens, there would be no indication whatsoever. No alerts, no logs, nothing. They could sit on these credentials and use them months later, long after they’ve left the company. We’ve seen this exact scenario play out recently with supply-chain attacks like the &lt;a href=&quot;https://socket.dev/blog/shai-hulud-strikes-again-v2&quot;&gt;Shai-Hulud npm takeover&lt;/a&gt; where attackers compromised maintainer accounts to publish malicious versions of popular packages.&lt;/p&gt;
&lt;h2 id=&quot;the-valet-key&quot;&gt;The Valet Key&lt;/h2&gt;
&lt;p&gt;Some fancy cars come with a “valet key” - a special key you give to parking attendants or car wash folks. Unlike your regular key, this one has limited capabilities: maybe it can only go up to 20mph, can’t open the trunk, or won’t let you disable the alarm. It’s the same car, but with reduced privileges for reduced risk of theft.&lt;/p&gt;
&lt;p&gt;This concept maps beautifully to our problem. Instead of giving everyone the full keys (the publishing tokens), why not give them a way to &lt;em&gt;request&lt;/em&gt; the car to be moved (a release be made)? The actual keys stay with a very small, trusted (and monitored) group who are the builders and maintainers of the infrastructure. Even the approvers don’t actually have access to the keys!&lt;/p&gt;
&lt;p&gt;Here’s what we wanted:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Secrets in a secure, limited-access location&lt;/strong&gt; - only 3-4 release engineers should have access&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear approval process&lt;/strong&gt; - every release needs explicit sign-off from authorized personnel&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Low friction for developers&lt;/strong&gt; - anyone should be able to &lt;em&gt;request&lt;/em&gt; a release easily&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full audit trail&lt;/strong&gt; - everything logged being compliance-friendly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No new infrastructure&lt;/strong&gt; - we didn’t want to build or maintain a separate secrets service&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As a side note, trusted publishing through OIDC and OAuth with limited and very short-lived tokens is the &lt;em&gt;actual&lt;/em&gt; digital equivalent of valet keys. npm is slowly rolling this out&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;, but at the time we built this system, it wasn’t an option. And even today, it’s not available at the organization/scope level which is what we’d need. Also, we publish way more places than npm so we need a more generic solution.&lt;/p&gt;
&lt;p&gt;Another approach worth mentioning is Google’s &lt;a href=&quot;https://github.com/GoogleCloudPlatform/wombat-dressing-room&quot;&gt;Wombat Dressing Room&lt;/a&gt; - an npm registry proxy that funnels all publishes through a single bot account with 2FA enabled. It’s a clever solution if you’re npm-only and want something off-the-shelf. That said it still requires running a separate service.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id=&quot;enter-getsentrypublish&quot;&gt;Enter getsentry/publish&lt;/h2&gt;
&lt;p&gt;The solution we landed on is beautifully simple in hindsight: a &lt;a href=&quot;https://github.com/getsentry/publish&quot;&gt;separate repository&lt;/a&gt; dedicated entirely to publishing. Here’s the trick:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Write access is extremely limited&lt;/strong&gt; - only 3-4 release engineers can actually modify the repo&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Release managers get “triage” access&lt;/strong&gt; - GitHub’s triage role lets you manage issues and labels, but not code - perfect for approving releases&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Everyone else can create issues&lt;/strong&gt; - that’s all you need to request a release&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Approval happens via labels&lt;/strong&gt; - a release manager adds the “accepted” label to trigger the actual publish&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The beauty of this setup is that the publishing tokens live &lt;em&gt;only&lt;/em&gt; in this repo’s secrets. The repo itself is mostly static - we rarely need to modify the actual code - so the attack surface is minimal.&lt;/p&gt;
&lt;h2 id=&quot;the-implementation-with-craft&quot;&gt;The Implementation (with Craft)&lt;/h2&gt;
&lt;p&gt;Under the hood, we use &lt;a href=&quot;https://github.com/getsentry/craft&quot;&gt;Craft&lt;/a&gt;, our CLI tool for managing releases. Craft was designed with a crucial architectural decision that predates the publish repo: it separates releases into two distinct phases - &lt;strong&gt;prepare&lt;/strong&gt; and &lt;strong&gt;publish&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;prepare&lt;/strong&gt; phase is where all the “dangerous” work happens: &lt;code&gt;npm install&lt;/code&gt;, build scripts, test runs, changelog generation. This phase runs in the SDK repository &lt;em&gt;without&lt;/em&gt; any access to publishing tokens. The resulting artifacts are uploaded to GitHub as, &lt;em&gt;well&lt;/em&gt;, build artifacts.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;publish&lt;/strong&gt; phase simply downloads these pre-built artifacts and pushes them to the registries. No &lt;code&gt;npm install&lt;/code&gt;, no build scripts, no arbitrary code execution - just download and upload. This dramatically reduces the attack surface during the privileged publishing step. Even if an attacker managed to inject malicious code into a dependency, it would only execute during the prepare phase which has no access to publishing credentials.&lt;/p&gt;
&lt;p&gt;This two-phase architecture is what makes supply-chain attacks like &lt;a href=&quot;https://socket.dev/blog/shai-hulud-strikes-again-v2&quot;&gt;Shai-Hulud&lt;/a&gt; much harder to pull off against Sentry’s SDKs. The malicious code would need to somehow persist through the artifact upload/download cycle and execute during a phase that deliberately runs no code.&lt;/p&gt;
&lt;p&gt;The magic happens with our GitHub Actions setup:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Developer triggers release workflow&lt;/strong&gt; in their SDK repo (e.g., &lt;a href=&quot;https://github.com/getsentry/sentry-javascript&quot;&gt;&lt;code&gt;sentry-javascript&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/getsentry/action-prepare-release&quot;&gt;&lt;code&gt;action-prepare-release&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; runs &lt;code&gt;craft prepare&lt;/code&gt;: creates the release branch, updates changelogs, builds artifacts, uploads them to GitHub&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An issue is automatically created&lt;/strong&gt; in &lt;code&gt;getsentry/publish&lt;/code&gt; with all the details: what changed, what’s being released, which targets&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Release manager reviews and approves&lt;/strong&gt; by adding the “accepted” label&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Publishing workflow triggers&lt;/strong&gt; &lt;code&gt;craft publish&lt;/code&gt;: downloads artifacts from GitHub and pushes to npm, PyPI, crates.io, etc. - no build step, just upload&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;fighting-overprotective-parents&quot;&gt;Fighting Overprotective Parents&lt;/h2&gt;
&lt;p&gt;GitHub, bless their security-conscious hearts, put up quite a few guardrails that we had to work around. Here’s where things got… creative:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Token Trigger Problem&lt;/strong&gt;: For the automation, we had to use the &lt;a href=&quot;https://github.com/apps/sentry-release-bot&quot;&gt;Sentry Release Bot&lt;/a&gt;, a GitHub App that generates short-lived tokens. This is crucial because &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; (default token GitHub Actions creates) has a security restriction: actions triggered by it don’t trigger other actions&lt;sup&gt;&lt;a href=&quot;#user-content-fn-4&quot; id=&quot;user-content-fnref-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;. We needed workflows in &lt;code&gt;getsentry/publish&lt;/code&gt; to trigger based on issues created from SDK repos, so we had to work around this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Admin Bot Account&lt;/strong&gt;: We needed a bot that could commit directly to protected branches. GitHub’s branch protection rules &lt;del&gt;are&lt;/del&gt; were all-or-nothing - you can’t say “this bot can commit, but only to update &lt;code&gt;CHANGELOG.md&lt;/code&gt;”. So our bot ended up with admin access on all repos. Not ideal, but necessary&lt;sup&gt;&lt;a href=&quot;#user-content-fn-5&quot; id=&quot;user-content-fnref-5&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Composite Actions and Working Directories&lt;/strong&gt;: If you’ve ever tried to use GitHub’s composite actions with custom working directories, you know the pain. There’s no clean way to say “run this composite action from this subdirectory”. We ended up with various hacks involving explicit &lt;code&gt;cd&lt;/code&gt; commands and careful path management.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Some More &lt;em&gt;Creative&lt;/em&gt; Workarounds&lt;/strong&gt;: We maintain a small collection of ugly-but-necessary workarounds in our action definitions. They’re not pretty, but they work. Sometimes pragmatism beats elegance&lt;sup&gt;&lt;a href=&quot;#user-content-fn-6&quot; id=&quot;user-content-fnref-6&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2 id=&quot;happily-ever-after&quot;&gt;Happily Ever After&lt;/h2&gt;
&lt;p&gt;After all this work, what did we actually achieve?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compliance-friendly&lt;/strong&gt; ✓ - every release is logged, approved, and traceable&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Centralized secrets&lt;/strong&gt; - tokens live in one place, accessible to very few&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer convenience&lt;/strong&gt; - anyone can request a release with a few clicks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enterprise security&lt;/strong&gt; - no individual has publishing credentials on their machine&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full transparency&lt;/strong&gt; - the entire &lt;a href=&quot;https://github.com/getsentry/publish&quot;&gt;publish repo&lt;/a&gt; is open, notifications enabled for stakeholders&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We’ve made more than &lt;strong&gt;6,000 releases&lt;/strong&gt; through this system and happily counting upwards. Every single one is traceable: who requested it, who approved it, what changed, when it shipped.&lt;/p&gt;
&lt;h2 id=&quot;why-this-matters-today&quot;&gt;Why This Matters Today&lt;/h2&gt;
&lt;p&gt;Recent supply-chain attacks like &lt;a href=&quot;https://socket.dev/blog/shai-hulud-strikes-again-v2&quot;&gt;Shai-Hulud&lt;/a&gt; show exactly why this architecture matters. When attackers compromise a maintainer’s npm account, they can publish malicious versions of packages that millions of developers will automatically install. With our system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No individual at Sentry has npm/PyPI/crates.io credentials on their machine&lt;/li&gt;
&lt;li&gt;Every release requires explicit approval from a release manager&lt;/li&gt;
&lt;li&gt;The approval happens in a public repo with full audit trail&lt;/li&gt;
&lt;li&gt;Any suspicious activity would be immediately visible&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Is it perfect? No. Could a determined attacker with inside access still cause damage? Probably. But we’ve dramatically reduced the attack surface and made any compromise immediately visible and auditable.&lt;/p&gt;
&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;Looking back, this is one of my proudest achievements at Sentry. It’s not flashy - no one’s going to write a blog post titled “Revolutionary New Way to Click a Label” - but it’s the kind of infrastructure that quietly makes everything more secure and more convenient at the same time.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-7&quot; id=&quot;user-content-fnref-7&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;If you’re dealing with similar challenges, I encourage you to check out &lt;a href=&quot;https://github.com/getsentry/publish&quot;&gt;getsentry/publish&lt;/a&gt; and the &lt;a href=&quot;https://github.com/getsentry/craft&quot;&gt;Craft&lt;/a&gt;. The concepts are transferable even if you don’t use our exact implementation.&lt;/p&gt;
&lt;p&gt;And hey, it only took me 5 years to write about it. Better late than never, right? 😅&lt;/p&gt;
&lt;h2 id=&quot;thanks&quot;&gt;Thanks&lt;/h2&gt;
&lt;p&gt;I’d like to thank the following people:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lucumr.pocoo.org/&quot;&gt;Armin&lt;/a&gt; and &lt;a href=&quot;https://danielgriesser.com/&quot;&gt;Daniel&lt;/a&gt; for their trust and support in building this system.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kamilogorek.com/&quot;&gt;Kamil&lt;/a&gt; for Craft as I knew it.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jeffreyhung.com/&quot;&gt;Jeffery&lt;/a&gt; for reviewing this post thoroughly and being my partner in crime for many things security at Sentry.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bsky.app/profile/selviano.bsky.social&quot;&gt;Michael&lt;/a&gt; for giving me the push I needed to write this post, coming up with the awesome post image idea, and for his support and guidance on the post itself.&lt;/li&gt;
&lt;/ul&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;This was before GitHub introduced “environment secrets” which allow more granular access control. Even with those, the problem isn’t fully solved for our use case. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;npm has OIDC support for individual packages, but not yet at the organization or scope level. See &lt;a href=&quot;https://docs.npmjs.com/trusted-publishers&quot;&gt;npm’s trusted publishers documentation&lt;/a&gt;. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;If only someone could make this run directly in GitHub Actions… &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;This is actually a smart security feature - imagine a workflow that creates a commit that triggers itself. Infinite loop, infinite bills, infinite sadness. &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-5&quot;&gt;
&lt;p&gt;This is now fixed with special &lt;a href=&quot;https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository?search-overlay-input=ruleset+exception&amp;#38;search-overlay-ask-ai=true#granting-bypass-permissions-for-your-branch-or-tag-ruleset&quot;&gt;by-pass rules via rule sets&lt;/a&gt; recently and we also no longer have admin access for the bots, phew. &lt;a href=&quot;#user-content-fnref-5&quot; data-footnote-backref aria-label=&quot;Back to reference 5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-6&quot;&gt;
&lt;p&gt;If you peek at the repo, you’ll see what I mean. I’m not proud of all of it, but I’m proud it works. &lt;a href=&quot;#user-content-fnref-6&quot; data-footnote-backref aria-label=&quot;Back to reference 6&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-7&quot;&gt;
&lt;p&gt;Especially while “security means more friction” is still a thing. &lt;a href=&quot;#user-content-fnref-7&quot; data-footnote-backref aria-label=&quot;Back to reference 7&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item><item><title>Marking it Up (and Down)</title><link>https://byk.im/posts/marking-it-up-and-down</link><guid isPermaLink="true">https://byk.im/posts/marking-it-up-and-down</guid><description>How we added markdown versions of all 8754 pages on Sentry Docs (and keep going)</description><pubDate>Wed, 02 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;first-there-was-plain-text&quot;&gt;First, there was plain text&lt;/h2&gt;
&lt;p&gt;When I first learned about &lt;a href=&quot;https://daringfireball.net/projects/markdown/&quot;&gt;Markdown&lt;/a&gt;, I was a bit skeptical. Why use &lt;em&gt;weird&lt;/em&gt; punctuation when you can use HTML instead? But as I started using it more,
especially on forums etc, I realized the power of it. Unlike HTML, it was way more accessible and easier to type. Even more importantly, it was still readable
and expressed meaning without obstructing the text before rendering. And slowly but surely, all major platforms, &lt;a href=&quot;https://faq.whatsapp.com/539178204879377/?cms_platform=web&amp;#38;locale=en_US&quot;&gt;including WhatsApp&lt;/a&gt; adopted it.&lt;/p&gt;
&lt;h2 id=&quot;the-age-of-ai&quot;&gt;The Age of AI&lt;/h2&gt;
&lt;p&gt;And then ChatGPT happened.
Due to the properties I listed above, Markdown was the perfect format for LLMs too. Once the agents hit the scene, they started generating Markdown
formatting and they were also more than happy to ingest Markdown formatted text for their context. There was only one problem though: the web revolved around
HTML, and some of that being dynamically generated. Even if you could teach or extract HTML, the dynamic JS part of it is still a challenge and usually requires
a full browser environment. Sure, there’s &lt;a href=&quot;https://github.com/microsoft/playwright-mcp&quot;&gt;Playwright MCP&lt;/a&gt; but it’s slow and resource-intensive. These challenges gave birth to services like &lt;a href=&quot;https://www.firecrawl.dev/&quot;&gt;Firecrawl&lt;/a&gt;
which I think is awesome, especially when you cannot control the source of the information.&lt;/p&gt;
&lt;p&gt;Recently, with a lot of &lt;del&gt;push&lt;/del&gt; help from &lt;a href=&quot;https://cra.mr/&quot;&gt;David&lt;/a&gt;, I started learning about agentic flows and how to use LLMs more than generating 0-shot responses&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. I wanted
to write a bit about these too but &lt;a href=&quot;https://blog.scottlogic.com/ceberhardt/&quot;&gt;Colin Eberhardt&lt;/a&gt; already did a great job with &lt;a href=&quot;https://blog.scottlogic.com/2023/05/04/langchain-mini.html&quot;&gt;Re-implementing LangChain in 100 lines of code&lt;/a&gt;. This is the article that made
it &lt;em&gt;click&lt;/em&gt; for me. Once I read it, I even felt a bit silly for expecting something more complex. It’s deceptively simple: you use the LLM in a loop, parse the responses
to trigger “tools”, and feed the results back (part of the loop) until you reach a final result (or the limit of your wallet). Anyway, great article, definitely go read
it. Let’s go back to talking about Markdown and its cousins as this is a blog post about that, not LLMs.&lt;/p&gt;
&lt;h2 id=&quot;walking-back-from-the-x-factor&quot;&gt;Walking back from the X-factor&lt;/h2&gt;
&lt;p&gt;For my new internal project, I needed to use &lt;a href=&quot;https://docs.sentry.io/&quot;&gt;our docs&lt;/a&gt;. Although they were already authored in MDX, it was not pure Markdown. We &lt;em&gt;can&lt;/em&gt; strip the MDX parts but Sentry
Docs are architected to share certain parts of the content between different pages. This means we actually have to render the MDX to get the full content. As a person who
spent some time around parsing out dependencies, building a dependency graph and working over it I had no interest in going down that path unless I really had to. So I
decided to look for an existing “HTML to Markdown” solution. This led me to the awesome &lt;a href=&quot;https://github.com/rehypejs/rehype-remark&quot;&gt;rehype-remark&lt;/a&gt; package which is a part of the &lt;a href=&quot;https://unifiedjs.com/&quot;&gt;unified&lt;/a&gt; project. I was
already quite familiar with unified and &lt;a href=&quot;https://github.com/remarkjs/remark&quot;&gt;remark&lt;/a&gt; which we also used in our docs rendering pipeline. So I simply jumped on this. My initial solution was simply to fetch
the page of interest, convert it into markdown, find the relevant header and extract the contents until the next header. &lt;a href=&quot;https://gist.github.com/BYK/d8b9bdba5d1ea9bc12fdfb2157d93854&quot;&gt;The code&lt;/a&gt; was also simple:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { Root, Heading } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;mdast&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; rehypeParse &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;rehype-parse&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; rehypeRemark &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;rehype-remark&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; remarkStringify &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;remark-stringify&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { unified } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;unified&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; extractMDSection&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;section&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;section&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; RegExp&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;tree&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Root&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; headingIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tree.children.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;findIndex&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;((&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;node&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;      return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        node.type &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;heading&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        node.children[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        node.children[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;].type &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;link&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        section?.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;test&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(node.children[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;].url)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; heading&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tree.children[headingIdx] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Heading&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; nextHeadingIdx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tree.children.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;findIndex&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;node&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;idx&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        idx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; headingIdx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        node.type &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;heading&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        node.depth &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; heading.depth&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    tree.children &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tree.children.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;slice&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      headingIdx,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      nextHeadingIdx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; undefined&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nextHeadingIdx&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tree;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getWebpageAsMarkdown&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; string&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;section&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?:&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; RegExp&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; response&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(url);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; text&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; response.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; String&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; unified&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(rehypeParse)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(rehypeRemark)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(extractMDSection, { section })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;use&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(remarkStringify)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;process&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(text)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;putting-it-everywhere-and-fast&quot;&gt;Putting it everywhere, and fast&lt;/h2&gt;
&lt;p&gt;Then &lt;a href=&quot;https://llmstxt.org/&quot;&gt;&lt;code&gt;/llms.txt&lt;/code&gt;&lt;/a&gt; happened. All players in the field who wanted to be more useful in the “age of AI”&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; started publishing their content accessible to LLMs, in plain text or more
commonly, Markdown format. Then a convention emerged: if you add &lt;code&gt;.md&lt;/code&gt; at the end of the URL you may get lucky and get the Markdown version of that page. I’m not sure when this kind of
convention started but it reminded me of the &lt;code&gt;.patch&lt;/code&gt; trick that &lt;a href=&quot;https://github.com/getsentry/sentry-docs/pull/13994.patch&quot;&gt;GitHub offers for their PRs&lt;/a&gt;. We wanted this for Sentry Docs! The first approach was to do this on the fly on a
specific route. Not only did this prove tricky to implement in NextJS, which our docs are built in, it also had an efficiency problem. Since we cannot go directly from MDX to Markdown,
we had to render the HTML from MDX first and then convert it to Markdown, essentially doubling the work. A nice trick &lt;a href=&quot;https://github.com/codyde&quot;&gt;Cody&lt;/a&gt; came up with was building the Markdown versions from
the static HTML files that NextJS generates during pre-rendering, putting them under the &lt;code&gt;public&lt;/code&gt; directory, and adding a &lt;code&gt;rewrite&lt;/code&gt; rule to NextJS to serve them when the &lt;code&gt;.md&lt;/code&gt; extension
is requested. This worked beautifully but created another issue: we had to generate the Markdown files for all 8754 pages in Sentry Docs and doing this takes a lot of time, up to 6-7 minutes.&lt;/p&gt;
&lt;p&gt;For a one-off job, spending several minutes is more than OK. But for a CI job that runs on every single commit, it is completely unacceptable. So I reached for 2 very old tricks used in every
build pipeline: caching and parallelization. The script for Markdown generation was refactored to spawn multiple &lt;a href=&quot;https://nodejs.org/api/worker_threads.html&quot;&gt;NodeJS Worker Threads&lt;/a&gt; for parallelization. Then I also added a very naive
cache which got the MD5 hash of the source HTML file and created a cache file with that name containing the converted Markdown. This allowed me to just do a &lt;code&gt;cp&lt;/code&gt; operation if the source file
did not change. These worked great on my local environment. The parallelization cut down the processing time by about 6x on my 16-core machine and the caching reduced that time by another 10x.
However, when I pushed this to Vercel, our hosting platform for Sentry Docs, it was still &lt;em&gt;very&lt;/em&gt; slow. Looking carefully at the build logs I noticed 2 issues:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Vercel build machines usually had 2 or 4 cores, significantly lower than 16.&lt;/li&gt;
&lt;li&gt;The cache was not being used at all!&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Solving the first one was not possible. During my tuning (local and on CI), I discovered we needed about half of the available cores due to the CPU &amp;amp; I/O intensive nature of the task:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// On a 16-core machine, 8 workers were optimal (and slightly faster than 16)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; numWorkers&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Math.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;max&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(Math.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;floor&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cpus&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;().&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I started investigating the cache issue and after several hours of digging, I finally realized what was going on. NextJS creates a new “signing secret” for every build which also affects
the file names of the JS files it generates as the names are created from file contents. This then causes the HTML files’ MD5 hashes to change although their actual contents were the same. To
overcome this in a cheap manner I had to strip the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags (along with their contents) from the HTML files before hash calculation and processing:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;ts&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; text&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; readFile&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(source, { encoding: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;utf8&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // Remove all script tags, as they are not needed in markdown&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;  // and they are not stable across builds, causing cache misses&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;replace&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;&amp;lt;script&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;^&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&amp;gt;]&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;[\s\S]&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*?&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#85E89D;font-weight:bold&quot;&gt;\/&lt;/span&gt;&lt;span style=&quot;color:#DBEDFF&quot;&gt;script&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;gi&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Surprisingly, this also reduced the processing time by about 2x as the HTML files were significantly smaller without the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags.&lt;/p&gt;
&lt;p&gt;We also started uploading these to a special Cloudflare R2 bucket for RAG processing that &lt;a href=&quot;https://x.com/zeeg/status/1938619824751653303&quot;&gt;David started using for a much better search experience&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;cant-stop-the-feeling&quot;&gt;Can’t Stop the Feeling!&lt;/h2&gt;
&lt;p&gt;Once I get into optimization mode, I cannot stop until I hit a very hard wall or actually get every ounce of optimization implemented. So I started looking at other places where I can use the
same old tricks of caching and parallelization. Turns out our MDX pipeline was not only uncached, it was also mostly using the &lt;code&gt;sync&lt;/code&gt; version of the file system APIs in NodeJS. So I made every
single file system operation async, used &lt;code&gt;Promise.all&lt;/code&gt; to parallelize them and got a huge speed increase. That is until this was shipped to Vercel. This time, it was the “dynamic pages” which
used &lt;a href=&quot;https://vercel.com/docs/functions/runtimes/node-js&quot;&gt;Vercel Functions&lt;/a&gt; that caused crashes. These were crashing with an &lt;code&gt;EMFILE&lt;/code&gt; file error, indicating that the file descriptor limit was reached. In hindsight, this is very obvious but
back at the time I had to dig around as these were not happening locally. It first looked like a silly limitation in AWS Lambda, which is what Vercel Functions are based on, but it turned out
to be a legitimate issue. With the top level &lt;code&gt;Promise.all&lt;/code&gt;, I was creating all 8600+ promises at once, which themselves triggered more open file handles. Again, obviously this is insane
so I reached out to another old friend: &lt;a href=&quot;https://github.com/sindresorhus/p-limit&quot;&gt;&lt;code&gt;p-limit&lt;/code&gt;&lt;/a&gt;&lt;sup&gt;&lt;a href=&quot;#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;. With a limit of 200 concurrent promises, we sailed on smoothly.&lt;/p&gt;
&lt;p&gt;Then I moved on to the caching bit which turned out to be a bit more tricky. We are using this other awesome package called &lt;a href=&quot;https://github.com/kentcdodds/mdx-bundler&quot;&gt;&lt;code&gt;mdx-bundler&lt;/code&gt;&lt;/a&gt;. It takes in an MDX file, discovers all its
dependencies, and bundles them together into a single JS file. Easy peasy, right? Just cache the output based on the input MD5 and we’re good! Well, almost. The catch is we ask the bundler to
emit the assets (mostly images) into a separate folder. This means we also need to cache these assets too. The solution became a file &lt;em&gt;and&lt;/em&gt; a directory, using the cache key as their names&lt;sup&gt;&lt;a href=&quot;#user-content-fn-4&quot; id=&quot;user-content-fnref-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;
where we copy everything in place when we find them. This chopped another 3-4 minutes off the build time when only a few files changed, which is the common case.&lt;/p&gt;
&lt;h2 id=&quot;tying-it-all-together&quot;&gt;Tying it all together&lt;/h2&gt;
&lt;p&gt;It took about 2 weeks and 12 PRs to tie all the loose ends but now not only do we have &lt;code&gt;.md&lt;/code&gt; versions of every single page in Sentry Docs, we also have better RAG-based search (still in-progress), and
faster builds (from ~16 minutes down to ~11 minutes). I love these kinds of intense periods where I can focus on a few high-impact things and just punch them out. Hopefully, there will be some
more in the coming weeks and months. Here’s a list of the important PRs we made to get here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/getsentry/sentry-docs/pull/13994&quot;&gt;feat(ai): Add .md extension to provide pages in markdown for LLMs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/getsentry/sentry-docs/pull/14096&quot;&gt;ci(md): Add caching to md-export script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/getsentry/sentry-docs/pull/14109&quot;&gt;ci(build): Parallelize and cache mdx pipeline - fix md cache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/getsentry/sentry-docs/pull/14171&quot;&gt;ci(md): Upload md files to R2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/getsentry/sentry-docs/pull/14193&quot;&gt;feat(md): Use page title as the top level title&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/getsentry/sentry-docs/pull/14196&quot;&gt;feat(md): Rewrite URLs to be absolute and to .md versions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;These are nothing short of amazing but without the agent loop and with 0-shot approaches, they are not very useful for the tasks I have at hand. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Yup, let’s cringe together. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Btw, I still refuse to believe &lt;a href=&quot;https://github.com/sindresorhus&quot;&gt;Sindre Sorhus&lt;/a&gt; is a real person. That is alien-level productivity and reach 🙇🏻‍♂️ &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;Well, they cannot be the same name as you cannot have a file and a directory with the same name in the same place. So I just added a suffix to the file name. &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item><item><title>The magic word: TaxYearEnd</title><link>https://byk.im/posts/uk-payroll-taxes</link><guid isPermaLink="true">https://byk.im/posts/uk-payroll-taxes</guid><description>A British folk story on automated payroll, income taxes, and long documents</description><pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This blog wasn’t supposed to be all horror stories but looks like life sends me quite a few recently. I’ve spent more than a week at the end of April
trying to fix an incorrect tax code with my employer on record (as Sentry does not have a UK entity yet). I was able to get it fixed… but not through
the company nor through their support team.&lt;/p&gt;
&lt;h2 id=&quot;the-payslip&quot;&gt;The Payslip&lt;/h2&gt;
&lt;p&gt;It was an unusually sunny April day in south east England when I got my first payslip of the 2026 tax year. I was looking forward to this as it would
be marking the end of a tax code that deducted extra tax from my salary and put me back onto the tax bracket and code where I was supposed to be. The
reason for this was me switching jobs in the middle of the tax year and HMRC&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; thinking I got a massive pay rise I wish I had but didn’t.
Thankfully, I got all that extra tax claimed back with the year-end self-assessment tax return but I digress. It was a sunny April day where I was shocked to
find out that not only my tax code was incorrect, it was actually worse than before! I was expecting to get more money but my paycheck was actually &lt;em&gt;lower&lt;/em&gt;
than the month before 😭. I was confused at first but quickly recovered and reached out to my EOR company’s support team. It was not May 1st yet so I may
have had a chance to get it corrected before the actual bank transfer. I could have wished for world peace instead.&lt;/p&gt;
&lt;h2 id=&quot;the-denial&quot;&gt;The Denial&lt;/h2&gt;
&lt;p&gt;The initial reactions from the support team can be summarized as:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Oh you poor entitled prick, so sad your tax code makes you pay more tax but just deal with it.&lt;/li&gt;
&lt;li&gt;Oh poor boy, you don’t know taxes and you are definitely not smart enough to understand this. Just accept what it is as we are doing what we are told by HMRC.&lt;/li&gt;
&lt;li&gt;Oh, you are still here? Go figure this out with HMRC, they told us to use this tax code.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I am a person who doesn’t easily give up, especially when I &lt;em&gt;think&lt;/em&gt; I’m right.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; I have already logged into my HMRC account when I got a tax code change
notification a few weeks ago and I was sure my tax code there was not the one I was seeing on my payslip. Nonetheless, I logged in again to see if anything
has changed. Surprise surprise: nothing has changed. Took a screenshot, also found the tax code change letter’s PDF after a long series of clicking around and shared
these in the support thread. Response? Your proof does not prove anything, you seem to be lying and even if it did and you weren’t lying, we just cannot change
your tax code manually. Go to HMRC.&lt;/p&gt;
&lt;h2 id=&quot;a-game-of-letters&quot;&gt;A game of letters&lt;/h2&gt;
&lt;p&gt;At this point, I was fuming. Also worth mentioning that after the initial few messages, the issue was escalated to the payroll team which seems to be only able
to respond to these messages once a day. I started digging into HMRC’s self-service portal, reading up on tax codes, how they change, how they are communicated
to your employer, and what your/my employer is responsible for. I found all the nice history and modern tech behind all this and essentially debugged my
employer’s payroll system for them. For free.&lt;/p&gt;
&lt;p&gt;Every time your tax code changes, HMRC sends a letter to you and to your employer about this change. This letter is called a P6 or a P9. The &lt;a href=&quot;https://www.gov.uk/employee-tax-codes/changes&quot;&gt;P6&lt;/a&gt; is sent when
you are a new employee or when your tax code changes. The &lt;a href=&quot;https://www.gov.uk/government/publications/p9x-tax-codes&quot;&gt;P9&lt;/a&gt; is sent when you are an existing employee and your tax code changes. The letter contains
your new tax code, the effective date of the change, and a few other details&lt;sup&gt;&lt;a href=&quot;#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;. So, for my case, I got two letters: one on February 2025 and one on April 2025.
The first one, from February, was for the coming tax year, 2026, instructing my employer to set my tax code to what I was seeing on my HMRC account. The second
one, from April, was for the tax year that just ended (2025) and setting my tax code retroactively to the one I was seeing on my payslip. Can you see what’s
going on here?&lt;sup&gt;&lt;a href=&quot;#user-content-fn-4&quot; id=&quot;user-content-fnref-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-fineprint&quot;&gt;The “fineprint”&lt;/h2&gt;
&lt;p&gt;So the “fineprint” here is, to which year the letter applies to. Turns out the letter explicitly states the tax year it applies to but HMRC usually don’t send
retroactive letters. So payroll automation systems are coded to only look at the “effective date” and “tax code” fields and ignore anything else&lt;sup&gt;&lt;a href=&quot;#user-content-fn-5&quot; id=&quot;user-content-fnref-5&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;. So from
the company’s perspective, they were correct. The HMRC indeed told them to set my tax code to the one on my payslip, effective April 2025 BUT FOR THE BLOODY&lt;sup&gt;&lt;a href=&quot;#user-content-fn-6&quot; id=&quot;user-content-fnref-6&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;
2025 TAX YEAR. The letter from February was for the 2026 tax year but it got overridden by the letter that came in April.&lt;/p&gt;
&lt;p&gt;After discovering this entire kerfuffle, I documented and explained all of it to the support team. Demanded my tax code to be fixed ASAP, their payroll system
checked and fixed, and my money returned. And of course they complied and we all lived happily ever after under the rainbows from the unicorns living in the
beautiful British countryside.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-7&quot; id=&quot;user-content-fnref-7&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id=&quot;being-practical&quot;&gt;Being practical&lt;/h2&gt;
&lt;p&gt;What I got in response was still the same: the system was automated, their system showing that the tax code must be changed to the incorrect one effective
April 2025, and if I have a problem with that I should contact HMRC to change my tax code. I was furious at this point, creating a local sun which caused
the heatwave we had here in the UK in May. Before digging deeper though, I decided to stop the bleeding. I called HMRC, explained the situation briefly, and
asked whether I can get another P6 issued. And unlike the other side of the story, what I got was a few cheerful clicks and “done, we issued a new P6”.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-8&quot; id=&quot;user-content-fnref-8&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;8&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-depth-of-the-rabbit-hole&quot;&gt;The depth of the rabbit hole&lt;/h2&gt;
&lt;p&gt;Armed with the calmness of my &lt;strong&gt;new&amp;amp;improved™&lt;/strong&gt; tax code and the knowledge of the electrons and photons carrying that new P6 letter’s contents being on their way to
my employer’s &amp;lt;insert expletive here&amp;gt; payroll system, I started digging for this “electronic PAYE system”. After a short period of Googling, I found out
about the “Data Provisioning Service” (DPS) and HMRC API. The DPS is the “electronic PAYE system” that HMRC uses to send tax code changes to employers. That mythical
thing I’ve been looking for! As usual with things related to big organizations, it was architected around SOAP and XML with a &lt;a href=&quot;https://assets.publishing.service.gov.uk/media/5c0a74aaed915d0b7268ee2a/DPS.pdf&quot;&gt;very long specification document&lt;/a&gt;.
That said, as expected from an accountable and transparent organization, there is plenty of public documentation and tooling. If you have a special interest in
overly long and complex specifications, I highly recommend reading &lt;a href=&quot;https://www.gov.uk/government/publications/paye-internet-submissions-outgoing-data-provisioning-service-technical-specifications&quot;&gt;all the documents and checking out the tooling&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Although I didn’t have this special interest, I did have the interest of &lt;a href=&quot;https://xkcd.com/386/&quot;&gt;proving someone wrong&lt;/a&gt;.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-9&quot; id=&quot;user-content-fnref-9&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;9&lt;/a&gt;&lt;/sup&gt; Upon reading these, I discovered the awesome
&lt;a href=&quot;https://www.gov.uk/government/publications/paye-internet-submissions-outgoing-data-provisioning-service-outgoing-xml-generator&quot;&gt;Outgoing XML Generator&lt;/a&gt; or OXG 4.5 as it is known among friends. This is an ugly tool written in Java that can generate the XML payloads that
HMRC sends to employers. After some more digging I was able to generate a sample P6 message to test. And wouldn’t you know, it had this &lt;code&gt;TaxYearEnd&lt;/code&gt; field in it!
Upong seeing this, I asked for the “raw” data of this message from the payroll team. It took them long enough to provide it but they finally did. And guess what?
The &lt;code&gt;TaxYearEnd&lt;/code&gt; field was indeed set to &lt;code&gt;2025&lt;/code&gt; vindicating my theory that the payroll system was the faulty part here. I nobly relayed this information back to the
support agent to be passed to the payroll team, with full confidence that they now will apologize, fix the issue, and investigate their systems.&lt;/p&gt;
&lt;p&gt;Instead, I just validated all those people who called me naive for all these years. The support agent came back with the stale old “we are sorry but we cannot change
your tax code” response without admitting to any wrongdoing on their part. Lucky for me, I already got that new P6 issued so instead of creating another heatwave, I
just sat down, looked at my screen in disbelief. Disbelief that not only such organizations can exist but they make a lot of money along the way.&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The month turned to June, I got my new payslip with the correct tax code along with all the extra tax I paid returned to me. Staring into the distance, I then sat down
to write this blog post which turned out to be a guide in implementing a payroll system that interfaces with HMPC’s DPS. In hindsight, I think I should have just called
the HMRC for a new P6 as that was the solution in the end. That said I don’t regret all the research I did and things I’ve learned. I only regret the anxiety and stress
during the period.&lt;/p&gt;
&lt;p&gt;Anyway, if you ever implement that payroll system, please make sure to honor the &lt;code&gt;TaxYearEnd&lt;/code&gt; field in the P6 messages.&lt;/p&gt;
&lt;p&gt;Thanks,&lt;/p&gt;
&lt;p&gt;A friend.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;The UK tax authority: Her/His Majesty’s Revenue &amp;amp; Customs — for some reason I thought this was Her Majesty’s Revenue Chest but turns out I was wrong. Still way cooler than &lt;em&gt;Internal Revenue Service&lt;/em&gt;. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;People who know me IRL are nodding in agony right now. Hey, I’m sorry. We don’t get to choose our personality traits. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Turns out the devil was hidden in a specific detail here. Oh I’m not spoiling it, keep reading please ☝🏻 &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;Don’t just read on, just think a bit. I can wait. Since this is a text that’s already written it doesn’t really cost any extra. Seriously, I can wait till the heat death of the universe. Okay okay, let’s get back? &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-5&quot;&gt;
&lt;p&gt;Well, they don’t ignore to &lt;em&gt;whom&lt;/em&gt; the letter applies to obviously 😅 &lt;a href=&quot;#user-content-fnref-5&quot; data-footnote-backref aria-label=&quot;Back to reference 5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-6&quot;&gt;
&lt;p&gt;Oh yes, I lived in the UK enough to start using &lt;em&gt;bloody&lt;/em&gt; instead of the f-word. Just in writing for now. &lt;a href=&quot;#user-content-fnref-6&quot; data-footnote-backref aria-label=&quot;Back to reference 6&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-7&quot;&gt;
&lt;p&gt;Just in case you haven’t noticed, this is sarcasm. And if you haven’t for reals, maybe you should read something else? &lt;a href=&quot;#user-content-fnref-7&quot; data-footnote-backref aria-label=&quot;Back to reference 7&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-8&quot;&gt;
&lt;p&gt;And a P6 that has an even “better” tax code for me! &lt;a href=&quot;#user-content-fnref-8&quot; data-footnote-backref aria-label=&quot;Back to reference 8&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-9&quot;&gt;
&lt;p&gt;That someone being my employer’s payroll system, and the stakes being me overtaxed for a year definitely played a role here. &lt;a href=&quot;#user-content-fnref-9&quot; data-footnote-backref aria-label=&quot;Back to reference 9&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item><item><title>Nightmare on Apple Street</title><link>https://byk.im/posts/apple-code-signing-x-platform</link><guid isPermaLink="true">https://byk.im/posts/apple-code-signing-x-platform</guid><description>Finding your way around Apple&apos;s code signing requirements without macOS</description><pubDate>Thu, 08 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;*Clicks fingers, clears throat*&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Okay, I have procrastinated on this post for, &lt;em&gt;checks notes&lt;/em&gt;, 3 months now. It’s time. Time to let go of all the bad memories and the pain.&lt;/p&gt;
&lt;p&gt;See, all I wanted to do was to &lt;a href=&quot;/posts/fossilize#a-wild-boss-appears-signing-and-notarizing-on-macos&quot;&gt;create a terminal application&lt;/a&gt; that you can just download and run on Linux, macOS, and Windows.
I also got a bit ambitious and wanted to create this app on, &lt;em&gt;*gasp*&lt;/em&gt;, Linux! And you know, it worked on Windows.
Yeah, &lt;em&gt;that&lt;/em&gt; Windows that every developer loves to hate but secretly uses one way or another. But macOS? No no no no no, tsk tsk tsk, not so fast little boy.
You need to sign &amp;amp; notarize your stuff, and you need to do it &lt;strong&gt;The Apple Way™&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-apple-way&quot;&gt;The Apple Way™&lt;/h2&gt;
&lt;p&gt;The very first thing Apple wants is &lt;del&gt;your money&lt;/del&gt; an Apple Developer account which will &lt;a href=&quot;https://developer.apple.com/programs/whats-included/&quot;&gt;set you back $99&lt;/a&gt;&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. Every year that is.
Oh, and you cannot &lt;em&gt;just&lt;/em&gt; create a developer account. You see, you need One Apple Account™.
If this is going to be a personal developer account, you &lt;em&gt;just&lt;/em&gt; need your name, email, phone number, and your address&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. If you are trying to enroll your organization, god help you: you need your &lt;a href=&quot;https://developer.apple.com/help/account/membership/D-U-N-S/&quot;&gt;D-U-N-S number&lt;/a&gt;.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Now that we have warmed up, it is time for you to create an identifier for your application.
They recommend using a reverse-domain like name: &lt;code&gt;com.my-company.my-app&lt;/code&gt;. I know, you just want to self-distribute a simple binary.
Yes, you still need the unique identifier. No, you cannot use &lt;code&gt;asdf&lt;/code&gt; or &lt;code&gt;foobar&lt;/code&gt;.
Okay head over to &lt;a href=&quot;https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle&quot;&gt;identifier creation page&lt;/a&gt; and get it over with please. I’ll &lt;code&gt;await&lt;/code&gt;.
I &lt;em&gt;think&lt;/em&gt; you leave the capabilities empty.&lt;/p&gt;
&lt;h2 id=&quot;switching-to-the-highway&quot;&gt;Switching to the Highway&lt;/h2&gt;
&lt;p&gt;After this step we need to add a certificate to our account. Now, if you have XCode, there’s a built in UI for this. But remember,
we don’t have access to macOS where XCode can only survive in. Hence we will go rogue and will create a certificate signing request
(CSR) using the command line. We need a “private key” to create a CSR so we’ll be creating that via the CLI too.
Might seem complicated but it is just answering a bunch of questions and shuffling some files around.&lt;/p&gt;
&lt;p&gt;For this, we’ll be needing 2 tools:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;openssl&lt;/code&gt;&lt;sup&gt;&lt;a href=&quot;#user-content-fn-4&quot; id=&quot;user-content-fnref-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/indygreg/apple-platform-rs/releases/latest&quot;&gt;&lt;code&gt;rcodesign&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let’s start with the private key:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;openssl&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; genrsa&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -out&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; private.pem&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 2048&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we have generated our private key in &lt;code&gt;private.pem&lt;/code&gt;, we can create the CSR using &lt;code&gt;rcodesign&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;rcodesign&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; generate-certificate-signing-request&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --pem-file&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; private.pem&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --csr-pem-file&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; csr.pem&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Okay, we have the signing request in &lt;code&gt;csr.pem&lt;/code&gt;. Now we head to the page where you can &lt;a href=&quot;https://developer.apple.com/account/resources/certificates/add&quot;&gt;add a certificate&lt;/a&gt;
and follow the steps below:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;From the gazillion options, select &lt;strong&gt;Developer ID Application&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Select &lt;code&gt;G2 Sub-CA (Xcode 11.4.1 or later)&lt;/code&gt; for &lt;strong&gt;Profile Type&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Upload the &lt;code&gt;csr.pem&lt;/code&gt; file we just created.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now we should arrive at a page saying “Download Your Certificate”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Save this file as &lt;code&gt;pass.cer&lt;/code&gt; next to the other ones and keep them safe.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Download Apple’s root certificate and convert to PEM format (Apple Worldwide Developer Relations Certification Authority)&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;wget&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; http://developer.apple.com/certificationauthority/AppleWWDRCA.cer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Convert that to PEM format&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;openssl&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; x509&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -inform&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; der&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -in&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; AppleWWDRCA.cer&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -out&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; AppleWWDRCA.pem&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Convert pass.cer to PEM format&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;openssl&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; x509&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -inform&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; der&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -in&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; pass.cer&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -out&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; pass.pem&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Note down your cert expiration date. You’ll need to do this entire dance again some days before this date:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;openssl&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; x509&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -in&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; pass.pem&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -noout&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -enddate&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now we are going to combine everything into a p12 file.
Make sure to replace &lt;code&gt;Company Name&lt;/code&gt; in the command line arguments below with your company name or your name.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;openssl&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; pkcs12&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -export&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -clcerts&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -inkey&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; pass.pem&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -in&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; pass.pem&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -certfile&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; AppleWWDRCA.pem&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -name&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;quot;Company Name&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -out&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; codesign.p12&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -passout&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; pass:&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We made our &lt;code&gt;codesign.p12&lt;/code&gt; file &lt;strong&gt;not&lt;/strong&gt; password protected so you can use it in your CI/CD pipeline without having to enter a password.
If you’d rather have it password protected, run the command above without &lt;code&gt;-passout pass:&lt;/code&gt; at the end.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Before finishing, we need to note down your Team ID:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;openssl&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; pkcs12&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -in&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; codesign.p12&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -nodes&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; grep&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; OU&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;At this point, you only need the final &lt;code&gt;codesign.p12&lt;/code&gt; file.&lt;/p&gt;
&lt;h2 id=&quot;apples-sacred-stamp-of-approval&quot;&gt;Apple’s Sacred Stamp of Approval&lt;/h2&gt;
&lt;p&gt;To be admitted to Apple’s sacred notarization service, you need to get an App Store Connect API key.
If you enjoy a good read from Apple go &lt;a href=&quot;https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api&quot;&gt;read their documentation&lt;/a&gt;.
For the twitchy ones, like myself:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Head to &lt;a href=&quot;https://appstoreconnect.apple.com/access/integrations/api&quot;&gt;API Key Creation&lt;/a&gt; page&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Click on the &lt;strong&gt;+&lt;/strong&gt; next to &lt;strong&gt;Active&lt;/strong&gt; at the top of the table.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enter a name like &lt;code&gt;Code Signing&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Put &lt;code&gt;Developer&lt;/code&gt; for the &lt;strong&gt;Access&lt;/strong&gt; field&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hit &lt;strong&gt;Generate&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Notice the &lt;strong&gt;Download&lt;/strong&gt; button in the last column for the key you just created (bottom row)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Download and save the key with the name &lt;code&gt;apikey.p8&lt;/code&gt; next to &lt;code&gt;codesign.p12&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Note the &lt;strong&gt;Key ID&lt;/strong&gt; somewhere&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Note the &lt;strong&gt;Issuer ID&lt;/strong&gt; somewhere. This is a separate section above the key table.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now let’s combine all these 3 into a single JSON file so we don’t have to manage them separately:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;rcodesign&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; encode-app-store-connect-api-key&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; codesign_key.json&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;issuer-i&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;key-i&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; apikey.p8&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;At this point you only need the &lt;code&gt;codesign_key.json&lt;/code&gt; file. This will be used for notarization.&lt;/p&gt;
&lt;h2 id=&quot;the-entitled-apps&quot;&gt;The Entitled Apps&lt;/h2&gt;
&lt;p&gt;To be able to get your app notarized, it needs to have “entitlements”. This is essentially letting Apple know
ahead of time, which sensitive APIs your application will be using. Then Apple’s servers will issue a “ticket”
for this specific version of your app and when someone tries to run it, it will be checked and restrained to
these limitations.&lt;/p&gt;
&lt;p&gt;Since I don’t have cybernetic powers, I cannot (yet) deduce which entitlements your app needs over a blog post.
That said I can at least make a recommendation. Since I did this for fossilized Node.js applications, &lt;a href=&quot;https://github.com/BYK/fossilize/blob/main/entitlements.plist&quot;&gt;I just
copied&lt;/a&gt; what Node.js used for itself.&lt;/p&gt;
&lt;p&gt;You can use this or create your own by picking and choosing from &lt;a href=&quot;https://developer.apple.com/documentation/bundleresources/entitlements&quot;&gt;the vast array of entitlements&lt;/a&gt;
that Apple offers. There’s also &lt;a href=&quot;https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution&quot;&gt;more excellent prose&lt;/a&gt;
for those to understand deeper and follow the Apple cult even closer.&lt;/p&gt;
&lt;p&gt;At the end of this section, I’ll just assume you have an &lt;code&gt;entitlements.plist&lt;/code&gt; file that is
&lt;a href=&quot;https://developer.apple.com/documentation/security/resolving-common-notarization-issues#Ensure-properly-formatted-entitlements&quot;&gt;properly formatted&lt;/a&gt;&lt;sup&gt;&lt;a href=&quot;#user-content-fn-5&quot; id=&quot;user-content-fnref-5&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;5&lt;/a&gt;&lt;/sup&gt; next to the binary
you want to sign and notarize.&lt;/p&gt;
&lt;h2 id=&quot;sign-here-please6&quot;&gt;Sign here please&lt;sup&gt;&lt;a href=&quot;#user-content-fn-6&quot; id=&quot;user-content-fnref-6&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/h2&gt;
&lt;p&gt;Now that we got everything we need for signing &lt;em&gt;and&lt;/em&gt; notarization, we can get to actual business.
Signing is quite straightforward but getting the notarization right took a few tries. Let’s start with signing:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;rcodesign&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; sign&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --team-name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;your_team_i&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;d&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --p12-file&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; codesign.p12&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --for-notarization&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -e&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; entitlements.plist&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;your_binary_pat&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;h&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you opted for a password-protected p12 file above, you can add &lt;code&gt;--p12-password &amp;lt;password&amp;gt;&lt;/code&gt; or
&lt;code&gt;--p12-password-file &amp;lt;password_file_path&amp;gt;&lt;/code&gt; at the end of the command above.&lt;/p&gt;
&lt;h2 id=&quot;knock-knock-knocking-on-notarys-door&quot;&gt;Knock Knock Knocking on Notary’s Door&lt;/h2&gt;
&lt;p&gt;Now that we have a signed binary, we will get it notarized. We already got the prerequisites by using the &lt;code&gt;--for-notarization&lt;/code&gt; and
&lt;code&gt;-e entitlements.plist&lt;/code&gt; parts above so we are in good hands. We still need to zip the file before though&lt;sup&gt;&lt;a href=&quot;#user-content-fn-7&quot; id=&quot;user-content-fnref-7&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;zip&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; app.zip&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;lt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;path_to_your_ap&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;p&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;rcodesign&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; notary-submit&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --api-key-file&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; codesign_key.json&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --wait&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; app.zip&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;rm&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; app.zip&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;were-done-here&quot;&gt;We’re Done Here&lt;/h2&gt;
&lt;p&gt;Yup, we really are done. At this point you can start distributing the signed binary. People using a macOS should be able to use it without errors or warnings.
If they double click on it (instead of running from a terminal), they may still see a security warning as we cannot “staple” the notarization tickets to plain binaries. To be able to do this you
need to package your app as a &lt;code&gt;.pkg&lt;/code&gt; or &lt;code&gt;.dmg&lt;/code&gt; file but I wasn’t (and still am not) interested in learning more Apple stuff so you’ll need to figure that part out
yourself.&lt;/p&gt;
&lt;p&gt;If you want to have this process on a CI/CD pipeline you need to remember a few things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make sure you don’t do signing and notarization on PR branches as that means anyone who can create a PR can generate and distribute a binary with their potentially
malicious changes and with &lt;em&gt;your&lt;/em&gt; signature on it.&lt;/li&gt;
&lt;li&gt;I don’t think you need to password-protect your p12 file but if you are using a service like GitHub you probably cannot store files as secrets. A quick hack for
this is to store the &lt;code&gt;base64&lt;/code&gt; encoded string versions of these 2 files (&lt;code&gt;codesign.p12&lt;/code&gt; and &lt;code&gt;codesign_key.json&lt;/code&gt;) as secrets. Then you
&lt;a href=&quot;https://github.com/getsentry/spotlight/blob/4f3e34a43e5d1949f664fc8ea88f84b1050274af/.github/workflows/build.yml#L61-L65&quot;&gt;&lt;code&gt;base64&lt;/code&gt; decode these into their respective files&lt;/a&gt;
and continue business as usual.&lt;/li&gt;
&lt;li&gt;Also, don’t forget to &lt;a href=&quot;https://github.com/getsentry/spotlight/blob/4f3e34a43e5d1949f664fc8ea88f84b1050274af/.github/workflows/build.yml#L96-L101&quot;&gt;store the &lt;em&gt;signed&lt;/em&gt; binary&lt;/a&gt; as the artifact of your build.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;resources&quot;&gt;Resources&lt;/h2&gt;
&lt;p&gt;I’ve used the excellent docs &lt;a href=&quot;https://gregoryszorc.com/&quot;&gt;Gregory Szorc&lt;/a&gt; created for his amazing &lt;a href=&quot;https://gregoryszorc.com/docs/apple-codesign&quot;&gt;&lt;code&gt;apple-codesign&lt;/code&gt;&lt;/a&gt; project.
I essentially summarized these two pages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://gregoryszorc.com/docs/apple-codesign/main/apple_codesign_certificate_management.html&quot;&gt;https://gregoryszorc.com/docs/apple-codesign/main/apple_codesign_certificate_management.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gregoryszorc.com/docs/apple-codesign/main/apple_codesign_getting_started.html&quot;&gt;https://gregoryszorc.com/docs/apple-codesign/main/apple_codesign_getting_started.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also found &lt;a href=&quot;https://gist.github.com/karnauskas/f76ab849224f22fc32961288266094a2&quot;&gt;this amazing gist for creating &lt;code&gt;pkpass.p12&lt;/code&gt; files&lt;/a&gt; from the GitHub user
&lt;a href=&quot;https://github.com/karnauskas&quot;&gt;karnauskas&lt;/a&gt; and used parts of it.&lt;/p&gt;
&lt;p&gt;Finally, I’ve used &lt;a href=&quot;https://stackoverflow.com/a/27497899/90297&quot;&gt;this little hack from StackOverflow&lt;/a&gt; for providing you with
a command for creating password-less p12 files from the get go.&lt;/p&gt;
&lt;h2 id=&quot;thanks&quot;&gt;Thanks&lt;/h2&gt;
&lt;p&gt;I’d like to thank my colleague &lt;a href=&quot;https://github.com/szokeasaurusrex&quot;&gt;Daniel Szoke&lt;/a&gt; for his help for establishing this entire flow and
proof-reading this post. I should have written this &lt;em&gt;before&lt;/em&gt; he also got the pain to get &lt;code&gt;sentry-cli&lt;/code&gt;
signed but hey, better late than never, right? 😅&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;You need to scroll all the way to the bottom to see this very unimportant detail. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Yep, I’m being snarky. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;First time I heard about it. I wish patience to people dealing with Apple. And no, I have no intention of learning more about this but you have that link there. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;If you don’t have &lt;code&gt;openssl&lt;/code&gt; around, just search for how you can install it. Should be as easy as &lt;code&gt;&amp;lt;package_manager&amp;gt; install openssl&lt;/code&gt; where &lt;code&gt;&amp;lt;package_manager&amp;gt;&lt;/code&gt; is &lt;code&gt;apt&lt;/code&gt; or &lt;code&gt;yum&lt;/code&gt; or something akin to those. &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-5&quot;&gt;
&lt;p&gt;Being a bit picky, are we dear Apple? &lt;a href=&quot;#user-content-fnref-5&quot; data-footnote-backref aria-label=&quot;Back to reference 5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-6&quot;&gt;
&lt;p&gt;and here, and here, and here, and here… &lt;a href=&quot;#user-content-fnref-6&quot; data-footnote-backref aria-label=&quot;Back to reference 6&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-7&quot;&gt;
&lt;p&gt;Don’t ask me why they cannot be bothered with on-the-fly zipping or HTTP content encoding etc., I don’t know. &lt;a href=&quot;#user-content-fnref-7&quot; data-footnote-backref aria-label=&quot;Back to reference 7&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item><item><title>Making your Node.js application last centuries</title><link>https://byk.im/posts/fossilize</link><guid isPermaLink="true">https://byk.im/posts/fossilize</guid><description>Creating self-contained &amp; dependency-free Node.js applications across platforms</description><pubDate>Wed, 12 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been working on &lt;a href=&quot;https://spotlightjs.com/&quot;&gt;Sentry Spotlight&lt;/a&gt; for the past several months. One of the things I wanted to do was to reduce the friction on trying out and adopting Spotlight.
You don’t need to know what Spotlight is (yet!) to enjoy this thriller but if you really must know, it is a local and offline debugging tool leveraging Sentry SDKs. It supports errors, traces, and very soon profiling data 🤞🏻.&lt;/p&gt;
&lt;h2 id=&quot;one-binary-to-rule-them-all&quot;&gt;One binary to rule them all&lt;/h2&gt;
&lt;p&gt;Now, where were we? Right, it was a bright San Francisco morning when I decided to create a self-contained binary for Spotlight that you could “just download” and run. Nothing else needed. Without such a binary, you either need to have &lt;code&gt;node&lt;/code&gt; &amp;amp; &lt;code&gt;npx&lt;/code&gt; or &lt;code&gt;docker&lt;/code&gt; on your system. I think we have enough haters for both (rightfully so). Besides, I wanted to make Spotlight accessible to everyone. For
instance, if you are an Android developer you probably neither have &lt;code&gt;node&lt;/code&gt; nor &lt;code&gt;docker&lt;/code&gt; on your system and have no reason to install any of them.&lt;/p&gt;
&lt;p&gt;We do have the Electron app, that said we only have it for macOS, and, I don’t really
like the idea of shipping an entire browser for an application that has a simple web
interface and works over HTTP.&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id=&quot;enter-nodejs-single-executable-applications&quot;&gt;Enter Node.js Single Executable Applications&lt;/h2&gt;
&lt;p&gt;So, I started looking into ways to create a self-contained binary for a Node.js application. I know tools exist for Python so I was hoping that there would be &lt;em&gt;something&lt;/em&gt; for Node.js too. I came by &lt;a href=&quot;https://github.com/nexe/nexe&quot;&gt;nexe&lt;/a&gt; and I was
about to give it a shot when I noticed this “&lt;a href=&quot;https://nodejs.org/api/single-executable-applications.html#single-executable-applications&quot;&gt;Node.js Single Executable Applications (SEA)&lt;/a&gt;” entry on Google. Sure enough, Node.js folks were adding exactly what I was looking for into Node.js itself! I quickly tried out the steps listed and started jumping up and down with some hideous dance moves in between when I got a working binary for Spotlight.&lt;/p&gt;
&lt;p&gt;It was a bit laborious but OK for a local test. To be able to actually use this in a fully-automated CI system, there were a few things that needed sorting out:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Spotlight server needs to become a single, dependency-free CommonJS file&lt;/li&gt;
&lt;li&gt;I need to ship the Spotlight frontend assets with the binary&lt;/li&gt;
&lt;li&gt;I need a maintainable script to do all the above and build the binary&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;single-file-nodejs-application-not-a-binary&quot;&gt;Single-file Node.js Application (not a binary)&lt;/h2&gt;
&lt;p&gt;Creating dependency-free CommonJS files is not something I’m unfamiliar with. I’ve first encountered this technique when I was working on &lt;a href=&quot;https://classic.yarnpkg.com/lang/en/&quot;&gt;Yarn&lt;/a&gt; quite a while ago. Back then, some smart folks at Facebook (nee Meta) realized they can pack a Node.js app into a single file just like a bundled web application&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. This was using a bundler such as Webpack (remember, this is 2015). I then used this technique on &lt;a href=&quot;https://github.com/getsentry/craft/&quot;&gt;Craft&lt;/a&gt; during my first stint at Sentry. This method already makes it easier to distribute and run a Node.js application without needing to install any dependencies. But it still requires &lt;code&gt;node&lt;/code&gt; to be installed on the system (and it needs the &lt;em&gt;correct&lt;/em&gt; version of it).&lt;/p&gt;
&lt;p&gt;Due to my past good memories from Craft, I chose &lt;a href=&quot;https://esbuild.github.io/&quot;&gt;esbuild&lt;/a&gt; as my trusty (and swift) bundler for the job. Just as I was thinking this was too easy, I found myself
on the sidelines of the great ESM vs CJS war. As an application built in the modern times, Spotlight is using ESM modules all around. This also meant no more pesky &lt;code&gt;__filename&lt;/code&gt; and &lt;code&gt;__dirname&lt;/code&gt; globals and using the new &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta&quot;&gt;&lt;code&gt;import.meta&lt;/code&gt;&lt;/a&gt; instead.
When you compile this into a CommonJS bundle naively, &lt;code&gt;import.meta&lt;/code&gt; becomes an empty object, making &lt;code&gt;import.meta.url&lt;/code&gt; undefined, making it impossible to determine where your script is running. Thankfully&lt;sup&gt;&lt;a href=&quot;#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;, I was &lt;a href=&quot;https://github.com/evanw/esbuild/issues/1492&quot;&gt;not the first person to bump into this&lt;/a&gt; and there was &lt;a href=&quot;https://github.com/evanw/esbuild/issues/1492#issuecomment-893144483&quot;&gt;a simple yet crude solution&lt;/a&gt; that I’d happily take.&lt;/p&gt;
&lt;h2 id=&quot;packing-the-frontend-assets-in&quot;&gt;Packing the frontend assets in&lt;/h2&gt;
&lt;p&gt;The assets needed for Spotlight’s UI are not much: just an HTML page and an accompanying JS bundle. The first thing I tried was to bake these in with hard-coded names which worked just fine.
But I was acutely aware that it was not future-proof at all. It is easy to add more resources to a frontend application: be it split JS chunks, some images, or separate CSS files.
I could just pack everything in the &lt;code&gt;dist&lt;/code&gt; folder where the assets were generated into, but currently, the Node SEA resources API does not have a discovery mechanism. If you know the name of the resource(s), you can read them but if you don’t GLHF.&lt;/p&gt;
&lt;p&gt;Luckily again, all the bundlers produce a &lt;code&gt;manifest.json&lt;/code&gt; file that lists all the resources they’ve generated and their relationship with each other. I could just read this file and pack all the resources listed in it along with the manifest file with the well-known name &lt;code&gt;manifest.json&lt;/code&gt;. This way, I could read the manifest file and discover all the resources I need to serve the UI. And that is exactly what I did.&lt;/p&gt;
&lt;p&gt;Now all that is left was codifying all this logic in a neat little script that I could run on my CI system and get a shiny new binary at the end. Or was it?&lt;/p&gt;
&lt;h2 id=&quot;a-wild-boss-appears-signing-and-notarizing-on-macos&quot;&gt;A wild boss appears: signing and notarizing on macOS&lt;/h2&gt;
&lt;p&gt;Of course, if it wasn’t for my arch nemesis, macOS, how could we have fun&lt;sup&gt;&lt;a href=&quot;#user-content-fn-4&quot; id=&quot;user-content-fnref-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;? Starting from macOS Catalina (circa 2019), Apple requires all applications to be signed and notarized to be able to run without any warnings. The signature is a
hard-requirement to be able to run the file at all whereas notarization is to remove the warning and prompt.&lt;/p&gt;
&lt;p&gt;Any security-conscious developer would not eschew code signing and maybe even some sort of permission grants. That said since this is Apple, the grand builder and guardian of walled gardens, the Apple-specific way of doing these are quite tyrannical. You need to have an Apple Developer account (only $99/annum!), you need
to have a Mac, you need to use XCode and its toolchain, and you need to have a lot of patience. I had none of these. I’m a creature of speed and efficiency and rebellion.
I &lt;em&gt;could&lt;/em&gt; run the signing portion on a macOS runner on GitHub Actions but I can create all the binaries (including Windows ones) on a Linux machine, with a neat list of target architectures. I just don’t want to split &lt;em&gt;just that part&lt;/em&gt; of the process.&lt;/p&gt;
&lt;p&gt;After a lot of reading, exploration, and trial &amp;amp; error, I discovered the minimal steps
and required files and certificates and secrets you need to get this done&lt;sup&gt;&lt;a href=&quot;#user-content-fn-5&quot; id=&quot;user-content-fnref-5&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;. I also remembered the ambitious project from &lt;a href=&quot;https://gregoryszorc.com/&quot;&gt;indygreg&lt;/a&gt;, opening Apple’s code signing black box to the masses and to other platforms: &lt;a href=&quot;https://github.com/indygreg/apple-platform-rs/&quot;&gt;apple-platform-rs&lt;/a&gt;.
Now, with the power of &lt;code&gt;rcodesign&lt;/code&gt;, I could sign and notarize my bespoke binaries
for macOS on the standard Linux CI machines.&lt;/p&gt;
&lt;p&gt;Take that, final boss!&lt;/p&gt;
&lt;h2 id=&quot;a-maintainable-script-tool-for-all-this&quot;&gt;A maintainable &lt;del&gt;script&lt;/del&gt; tool for all this&lt;/h2&gt;
&lt;p&gt;With all the stuff built in, my “simple” build script became a &lt;a href=&quot;https://github.com/getsentry/spotlight/blob/bb7a499e5f95db84bab3c2929762ddc87cf36350/packages/spotlight/build.js&quot;&gt;~200-line monster&lt;/a&gt; with a few support files around. It was somewhat generalized but not enough for me to share it easily with others to prevent further suffering. This is why I decided to create a tool that would encapsulate all this logic and make it easy for anyone to create a self-contained binary for their Node.js application: presenting &lt;a href=&quot;https://github.com/BYK/fossilize&quot;&gt;fossilize&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Fossilize does all the things above, including macOS signing and auto-discovery of assets through a Vite-compatible &lt;code&gt;manifest.json&lt;/code&gt; file. It also caches the Node.js binaries it downloads to speed things up on repeated builds. It supports using different Node.js versions and understands a few simple aliases such as &lt;code&gt;local&lt;/code&gt;, &lt;code&gt;latest&lt;/code&gt;, and &lt;code&gt;lts&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;One irony is &lt;code&gt;fossilize&lt;/code&gt; itself cannot be fossilized at the moment due to some of its dependencies requiring dynamically determined native binaries per platform and some obscure issue with &lt;a href=&quot;https://github.com/nodejs/postject&quot;&gt;postject&lt;/a&gt; not being able to postject code containing itself.
I’m planning to tackle these with the help of WASM but for now, I think &lt;code&gt;fossilize&lt;/code&gt; is in a good place to serve the need.&lt;/p&gt;
&lt;p&gt;Onwards 🚀&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;Yet I happily use VS Code and Slack. Oh the hypocrisy! &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;They also did even smarter things like code caching to speed up start up times. Node SEA also &lt;a href=&quot;https://nodejs.org/api/single-executable-applications.html#v8-code-cache-support&quot;&gt;supports this&lt;/a&gt;. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Or unfortunately, depending on how you look at it. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;Hoping your definition of fun includes several days of trial &amp;amp; error, reading docs written as if you have to use Apple devices competently with an ambition of reaching Lord of the Rings levels of prose, and some late nights. &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-5&quot;&gt;
&lt;p&gt;A blog post dedicated to this journey is &lt;del&gt;being written as of this writing&lt;/del&gt; &lt;a href=&quot;/posts/apple-code-signing-x-platform&quot;&gt;available now&lt;/a&gt;. &lt;a href=&quot;#user-content-fnref-5&quot; data-footnote-backref aria-label=&quot;Back to reference 5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item><item><title>Docker Volume Caching on GitHub Actions</title><link>https://byk.im/posts/docker-volume-caching-gha</link><guid isPermaLink="true">https://byk.im/posts/docker-volume-caching-gha</guid><description>A new GitHub actions to cache your Docker volumes for faster and cheaper builds.</description><pubDate>Tue, 14 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I joined Sentry to exclusively work on their &lt;a href=&quot;https://github.com/getsentry/self-hosted&quot;&gt;self-hosted product&lt;/a&gt; in 2019. Back then, Sentry was just using a few services:
Postgres, Memcached, Redis, and Sentry itself. But it was on the cusp of becoming a multi-service application with the introduction of &lt;a href=&quot;https://github.com/getsentry/snuba&quot;&gt;Snuba&lt;/a&gt;
and along with that Kafka, &lt;a href=&quot;https://github.com/getsentry/relay&quot;&gt;Relay&lt;/a&gt;, &lt;a href=&quot;https://github.com/getsentry/symbolicator&quot;&gt;Symbolicator&lt;/a&gt; and others. Because it was supposed to be simple,
self-hosted (or &lt;a href=&quot;https://github.com/getsentry/onpremise/&quot;&gt;onpremise&lt;/a&gt; as it was called back then) did not have any tests or even any automation: just a bunch of instructions and
commands to run in the README. With the rapid increase in the number of engineers working on Sentry and the changes being made, it was clear that we needed to automate the testing
and setup of the self-hosted repository.&lt;/p&gt;
&lt;p&gt;To summarize about a year’s worth of work: we created an install script based in bash (as that was the most common denominator across all platforms), and a very cursory test suite
which ran the install script, tried to ingest an event, and read it back. The entire test suite took about 5-6 minutes to run and about half of that time was spent on running
Django migrations, from scratch, on a fresh database, over, and over, and over. The thing is we didn’t even add migrations frequently but we still had to run them all to get the
service up and running.&lt;/p&gt;
&lt;p&gt;The solution was obviously caching but caching Docker volumes was not really a thing that seemed feasible back then. Remember, this is 2019-2020, GitHub Actions was still in its infancy.
I was also barely getting comfortable with all that Bash and Docker stuff. Then I got distracted by other things, changed jobs, and eventually came back to Sentry to see that this was
still a problem. So I decided to tackle it head-on. I was going to cache the hell out of those Docker volumes for our databases.
We already had &lt;a href=&quot;https://github.com/actions/cache/&quot;&gt;&lt;code&gt;actions/cache&lt;/code&gt;&lt;/a&gt; now so how hard could it be? Famous last words.&lt;/p&gt;
&lt;p&gt;I have spent about 2 weeks to completely figure this out. About 50% of this was my ignorance about basic Linux tools such as &lt;code&gt;tar&lt;/code&gt;, file/directory permissions, and Docker’s
way of storing volumes. About 30% was me not trying things locally properly and just pushing to CI and waiting for the results. The remaining 20% was the actual hard parts to figure
out, mostly thanks to &lt;a href=&quot;https://stackoverflow.com/&quot;&gt;StackOverflow&lt;/a&gt; (yeah, still not on that “ChatGPT for everything” bandwagon&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;). I’ll summarize some of the findings here so you don’t
have to go through the same pain as I did:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Docker volumes are stored under &lt;code&gt;/var/lib/docker/volumes&lt;/code&gt; (by default, and please don’t change it)&lt;/li&gt;
&lt;li&gt;You cannot &lt;code&gt;stat&lt;/code&gt; a directory or anything under it if you don’t have &lt;code&gt;x&lt;/code&gt; permission on the directory itself (╯°□°)╯︵ ┻━┻&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tar&lt;/code&gt; &lt;em&gt;does&lt;/em&gt; preserve permissions and ownership by default but only if you are running it as root (or with &lt;code&gt;sudo&lt;/code&gt;) &lt;em&gt;(╯°□°)╯︵ ┻━┻ x 2&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tar&lt;/code&gt; preserves ownership information as names and not as IDs so if your Docker container uses a user id like &lt;code&gt;1000&lt;/code&gt;, GLHF &lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; &lt;strong&gt;(╯°□°)╯︵ ┻━┻ x 3&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Linux (Unix?) fs permissions are not just &lt;code&gt;rwx&lt;/code&gt; but there’s also an &lt;code&gt;s&lt;/code&gt; you can set on executables to allow them to set ownership of &lt;em&gt;other&lt;/em&gt; things&lt;sup&gt;&lt;a href=&quot;#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; ＼（〇_ｏ）／&lt;/li&gt;
&lt;li&gt;Not only GitHub Actions doesn’t run &lt;code&gt;tar&lt;/code&gt; with &lt;code&gt;sudo&lt;/code&gt;, and not only it &lt;a href=&quot;https://github.com/actions/toolkit/issues/946&quot;&gt;&lt;em&gt;refuses&lt;/em&gt;&lt;/a&gt; to do this, it also doesn’t allow you to run &lt;code&gt;tar&lt;/code&gt; with &lt;code&gt;--same-owner&lt;/code&gt; or &lt;code&gt;--numeric-owner&lt;/code&gt; &lt;strong&gt;&lt;em&gt;(╯°□°)╯︵ ┻━┻ x 4&lt;/em&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Bonus: there are these awesome tools called &lt;code&gt;getfacl&lt;/code&gt; and &lt;code&gt;setfacl&lt;/code&gt; that lets you backup and restore ACLs BUT NOT OWNERSHIP INFORMATION &lt;del&gt;&lt;strong&gt;&lt;em&gt;(╯°□°)╯︵ ┻━┻ x 5&lt;/em&gt;&lt;/strong&gt;&lt;/del&gt;&lt;/li&gt;
&lt;li&gt;Bonus 2: &lt;code&gt;mv&lt;/code&gt; would happily overwrite your target without even mentioning, especially if you use &lt;code&gt;sudo&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So, with all this information, what is needed to cache Docker volumes on GitHub Actions and restore them properly? Let’s see:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Set &lt;code&gt;+x&lt;/code&gt; permission on &lt;code&gt;/var/lib/docker&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;+rx&lt;/code&gt; permission on &lt;code&gt;/var/lib/docker/volumes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;u+s&lt;/code&gt; permission on &lt;code&gt;tar&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;tar --numeric-owner&lt;/code&gt; to create the archive — oh wait, you can’t because &lt;code&gt;actions/cache&lt;/code&gt; doesn’t let you (╯°□°)╯︵ ┻━┻&lt;sup&gt;(╯°□°)╯︵ ┻━┻&lt;sup&gt;(╯°□°)╯︵ ┻━┻&lt;sup&gt;(╯°□°)╯︵ ┻━┻&lt;/sup&gt;&lt;/sup&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;side-quest-hacking-tar-on-github-actions&quot;&gt;Side quest: Hacking &lt;code&gt;tar&lt;/code&gt; on GitHub Actions&lt;/h2&gt;
&lt;p&gt;Once I realized that I had to change the options passed to &lt;code&gt;tar&lt;/code&gt;, I &lt;em&gt;very reluctantly&lt;/em&gt; decided to “wrap” the actual &lt;code&gt;tar&lt;/code&gt; executable:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; cp&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /usr/bin/tar&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /usr/bin/tar.orig&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; echo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#39;exec tar.orig --numeric-owner -p --same-owner &amp;quot;$@&amp;quot;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /usr/bin/tar&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Oh, but wait, you cannot &lt;code&gt;sudo&lt;/code&gt; redirect output to a file as sudo just runs the command and redirection is done by the shell which you are not running as root. Let’s try that again:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; cp&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /usr/bin/tar&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /usr/bin/tar.orig&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;echo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &amp;#39;exec /usr/bin/tar.orig --numeric-owner -p --same-owner &amp;quot;$@&amp;quot;&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; sudo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; tee&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /usr/bin/tar&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /dev/null&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once I added this monstrosity, my GitHub Actions runs… started to hang indefinitely. Can you see the issue? ಠಿ_ಠ
Well, I couldn’t. I spent about 2 hours trying to figure out why this was happening. I suspected &lt;code&gt;exec&lt;/code&gt; might be the culprit and when I removed it, the runs at least started crashing with an error: &lt;code&gt;cannot fork&lt;/code&gt;. What?
Well, see I was doing this both in my &lt;code&gt;restore&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;save&lt;/code&gt; actions. So, when the &lt;code&gt;restore&lt;/code&gt; action ran, it wrapped/replaced &lt;code&gt;tar&lt;/code&gt; but then did not restore the original back. After some time, &lt;code&gt;save&lt;/code&gt; action ran trying to
do the same. Now remember our “Bonus 2” learning from above: when &lt;code&gt;save&lt;/code&gt; &lt;em&gt;also&lt;/em&gt; backed up &lt;code&gt;tar&lt;/code&gt; (which was actually my wrapper script) to &lt;code&gt;/usr/bin/tar.orig&lt;/code&gt;, &lt;code&gt;mv&lt;/code&gt; didn’t even flinch when &lt;code&gt;tar.orig&lt;/code&gt; already existed. Now
I had 2 copies of my wrapper script where the second one just &lt;code&gt;exec&lt;/code&gt;ed itself. Nice fork bomb there, me&lt;sup&gt;&lt;a href=&quot;#user-content-fn-4&quot; id=&quot;user-content-fnref-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;img src=&quot;https://byk.im/_astro/fork-bomb.BKc6HNhp_ZC6Oc1.webp&quot; alt=&quot;A smiling bomb with a fork stuck to it.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;640&quot; height=&quot;640&quot; class=&quot;dark:invert&quot;&gt;
&lt;p&gt;Once the fork bomb was defused, I was able to run &lt;code&gt;actions/cache&lt;/code&gt; and viola! My volumes were cached and restored properly. Space time is saved Marty!&lt;/p&gt;
&lt;h2 id=&quot;final-boss&quot;&gt;Final boss&lt;/h2&gt;
&lt;p&gt;After all this, I was still not very happy as it made all &lt;code&gt;action/cache&lt;/code&gt; calls in my workflow doubled, and with the same hack repeated in both parts. So I decided to create a GitHub Action that would contain the chaos, the
madness, the fork bomb minefield, and all the other ugliness. Both from my sight and others’. Please enjoy &lt;a href=&quot;https://github.com/BYK/docker-volume-cache-action&quot;&gt;BYK/docker-volume-cache-action&lt;/a&gt; and cache responsibly.&lt;/p&gt;
&lt;img src=&quot;https://byk.im/_astro/ci-minutes-saved.CL2blTQQ_Z22REPx.webp&quot; alt=&quot;A repeated CI run which took about 13 minutes versus 16 minutes without the cache.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;750&quot; height=&quot;462&quot;&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;That said all images for this article was generated by &lt;a href=&quot;https://deepai.org/machine-learning-model/text2img&quot;&gt;DeepAI Image Generator&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Looking at you &lt;a href=&quot;https://hub.docker.com/r/confluentinc/cp-kafka&quot;&gt;confluentinc/cp-kafka&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Yes, yes, there are &lt;em&gt;even&lt;/em&gt; more. Can you believe it? I couldn’t either. But I digress. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;Me when I realized this: mother forking shirt balls! &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item><item><title>Having a Good Ol&apos; RSS Feed in Astro</title><link>https://byk.im/posts/good-old-rss-feed-in-astro</link><guid isPermaLink="true">https://byk.im/posts/good-old-rss-feed-in-astro</guid><description>How to get a proper RSS feed for your blog in Astro with full content and images.</description><pubDate>Mon, 30 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;After reviving this blog with &lt;a href=&quot;https://astro.build&quot;&gt;Astro&lt;/a&gt;, I realized that I didn’t have an RSS feed even though the theme I’m using already has support for that.
So I got to work to enable it. For some reason, I just did not have the &lt;code&gt;rss.xml&lt;/code&gt; file generated. After much trial and error, I finally figured out that the method name in the
&lt;a href=&quot;https://github.com/palmiak/pacamara-astro/blob/cbff90909afbf4fa08fdfd47c860d4c732b00330/src/pages/rss.xml.js#L5&quot;&gt;endpoint definition&lt;/a&gt; should be &lt;code&gt;ALL_CAPS&lt;/code&gt; as in &lt;code&gt;GET&lt;/code&gt;
instead of &lt;code&gt;get&lt;/code&gt;. I’m guessing this was because of a major Astro version upgrade since &lt;a href=&quot;https://pacamara-astro-6y7xr.kinsta.page/&quot;&gt;the demo page for Pacamara&lt;/a&gt; has a working RSS feed.
Fixed that and problem solved, right? RIGHT?&lt;/p&gt;
&lt;p&gt;Sort of.&lt;/p&gt;
&lt;p&gt;Yes, I got &lt;em&gt;a&lt;/em&gt; feed but I noticed 3 major problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The feed did not have the full content of the posts&lt;/li&gt;
&lt;li&gt;The feed did not limit the number of entries&lt;/li&gt;
&lt;li&gt;The feed did not sort the posts in any way&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Although unsorted and uncapped posts was not a big deal as I only had 3 posts at the time, it looked like an easy fix so I started with that:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; MAX_ITEMS&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; posts&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; getCollection&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;quot;posts&amp;quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;sort&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(descDateSort)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  .&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;slice&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;MAX_ITEMS&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Quite straightforward: get all the posts, sort by date in descending order, and slice the first 10. Don’t know why this is not the default or a least is offered through a built-in helper, but let’s move onto the bigger issue.&lt;/p&gt;
&lt;p&gt;Getting the full content of the posts in RSS was much trickier as the RSS endpoint was defined as an &lt;a href=&quot;https://docs.astro.build/en/guides/endpoints/#static-file-endpoints&quot;&gt;“endpoint”&lt;/a&gt; and was not able to render Astro components.
Even &lt;a href=&quot;https://docs.astro.build/en/recipes/rss/&quot;&gt;the recipe for RSS on Astro docs&lt;/a&gt; says this is only possible for Markdown only &lt;em&gt;and&lt;/em&gt; it uses a custom Markdown renderer 🤯.
But I was determined, and was a devotee of “the search church” so off I went to find a solution.&lt;/p&gt;
&lt;p&gt;Although there was this &lt;a href=&quot;https://scottwillsey.com/rss-pt2/&quot;&gt;very creative solution&lt;/a&gt;, I bumped into a more straightforward one first: &lt;a href=&quot;https://blog.damato.design/posts/astro-rss-mdx/&quot;&gt;https://blog.damato.design/posts/astro-rss-mdx/&lt;/a&gt;.
This solution uses the new and experimental &lt;a href=&quot;https://docs.astro.build/en/reference/container-reference/&quot;&gt;Astro Containers&lt;/a&gt; to be able to render an Astro component in isolation inside the endpoint.
I followed the instructions and voilà! I had a working RSS feed with full content indeed. &lt;a href=&quot;https://github.com/BYK/byk.github.io/commit/0449bf42da53a98f72dbafe5e915fa6a4f530eba&quot;&gt;Committed&lt;/a&gt;, pushed, and &lt;a href=&quot;https://github.com/BYK/byk.github.io/actions/runs/12537931496/job/34962544900&quot;&gt;got yelled at by GitHub Actions&lt;/a&gt; with the following cryptic error:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;shell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;cannot&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; test&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; case&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; insensitive&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; FS,&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; CLIENT_ENTRY&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; does&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; point&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; to&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; an&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; existing&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; file:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /home/runner/work/byk.github.io/byk.github.io/dist/client/client.mjs&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I ran &lt;code&gt;npm run build&lt;/code&gt; on my local terminal immediately, and was able to reproduce the error locally. At least, I was not going to play the “try blind commits to see what the CI says” game.&lt;/p&gt;
&lt;p&gt;After searching for this error for about an hour, I realized that &lt;em&gt;something&lt;/em&gt; was triggering a client-side render mode in Vite (Astro’s underlying bundler) and I started to remove every single new line of code I added.
Indeed, once I disabled the import for both &lt;code&gt;@astrojs/container&lt;/code&gt; and &lt;code&gt;@astrojs/mdx&lt;/code&gt; the build error disappeared. As to why this was happening, I still had no idea. I kept digging and finally found this random (and very helpful)
message on the Astro Containers Stage 3 proposal thread: &lt;a href=&quot;https://github.com/withastro/roadmap/pull/916#issuecomment-2256059117&quot;&gt;https://github.com/withastro/roadmap/pull/916#issuecomment-2256059117&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// astro.config.mjs -- add the following&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  vite&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    ssr&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;      external&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;astro/container&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&amp;#39;@astrojs/mdx&amp;#39;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Of course this makes sense! Without the above configuration, Vite tries to put these Astro packages into a client bundle whereas I am strictly operating in a server-side rendering world. Once this is in, the build error went away with MDX rendering still intact.
I quickly pushed the code, got my deploy, and had my RSS feed! 🎉&lt;/p&gt;
&lt;p&gt;I wanted to test my feed before declaring a complete victory so I loaded it up in &lt;a href=&quot;https://readwise.io/i/burak13&quot;&gt;Readwise Reader&lt;/a&gt;, my RSS reader of choice, and saw that the images were not loading. I quickly realized that I (well, Astro) was using
relative paths for the images and that’s simply not how RSS works! I had to make all these image URLs absolute which was supposed be quite straightforward.&lt;/p&gt;
&lt;p&gt;For some reason, I couldn’t find that simple answer after much searching.
Then I tried to hook into the MDX pipeline to modify the URLs only to be disappointed as the image URLs are generated much later in the process and all I got was a JS identifier for the image source 🤦🏻‍♂️.
After more research, I learned all about Astro’s image processing pipeline, found out about its &lt;code&gt;getURL()&lt;/code&gt; method, dug into its source code and
&lt;a href=&quot;https://github.com/withastro/astro/blob/ebe2aa95c7f4a6559cec8b82d155da34a57bdd53/packages/astro/src/assets/services/service.ts#L370&quot;&gt;finally saw that it uses &lt;code&gt;import.meta.env.BASE_URL&lt;/code&gt;&lt;/a&gt; as the base!&lt;/p&gt;
&lt;p&gt;Easy, I thought: I’ll just set that in the config under &lt;code&gt;vite: {base: &amp;#39;...&amp;#39;}&lt;/code&gt;. That didn’t work. Then I tried setting it on the top-level Astro config only to be disappointed again. I also tried some other, sillier things that I don’t want to admit doing here.
Finally, like really finally, I found the answer in &lt;a href=&quot;https://docs.astro.build/en/reference/configuration-reference/#buildassetsprefix&quot;&gt;&lt;code&gt;build.assetsPrefix&lt;/code&gt;&lt;/a&gt;! Set this to my blog’s main URL, tested in dev mode to make sure it still works, got a full build,
checked the &lt;code&gt;rss.xml&lt;/code&gt; output and saw that the image URLs were now absolute! 🎉🎉🎉&lt;/p&gt;
&lt;p&gt;So, if you ever want the same (Astro blog with full content RSS feed and working images), I hope I can save you some hours with this post.&lt;/p&gt;
&lt;p&gt;Oh, by the way, if you want to &lt;a href=&quot;/rss.xml&quot;&gt;subscribe to my blog&lt;/a&gt;, now you can 😏&lt;/p&gt;</content:encoded></item><item><title>Roots</title><link>https://byk.im/posts/roots</link><guid isPermaLink="true">https://byk.im/posts/roots</guid><description>I&apos;ve got no roots, but my home was never on the ground</description><pubDate>Wed, 25 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It started when I was about to turn 6. The day when we were moving from my first home. I was losing everything, including our kitchen sink&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. The sink I was using since I was born.&lt;/p&gt;
&lt;p&gt;Then I started school, started biking, and it was all good. For a while. I even had a girlfriend&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. Then I changed schools, lost some of my friends, and definitely lost my girlfriend of 1 day.
Resettling took a few years. We moved 2 times during this time but the school didn’t change, hence the friends stayed.&lt;/p&gt;
&lt;p&gt;And then I changed schools again. Lost most of my friends again. I was not terrible at making new friends but the churn takes its toll.
Some years passed, then we moved again and came university. The great thing about university is that it lasts &lt;em&gt;at least&lt;/em&gt; 4 years&lt;sup&gt;&lt;a href=&quot;#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; and they tend to stay in the same place.
This definitely helped me to settle down a bit. I got my first job, made a few lifelong friends, met and started dating my future wife, and started to feel like I belong somewhere.
On my 5th year of university though, the chaos started again: most of my friends graduated, I switched jobs, and I was very close to graduating.
Then I graduated, got engaged, moved to yet another city for compulsory military service, then got married, moved to San Francisco for a new job, and then moved back to Izmir just after 11 months.&lt;/p&gt;
&lt;p&gt;I have never been able to stay in one place (city or home) for more than about 3 years since I was 6 and I am 36. That’s a lot of years and a lot of movement.
There’s no single reason for this. We moved a lot when I was a kid as my father was a military officer, then I moved to become a real adult, then I had to move across countries for work.
With every move, you lose friends along with some part of you. Settling into a new place can be extra hard when it is an entirely different country, let alone a city. New people, new culture, new everything.&lt;/p&gt;
&lt;p&gt;I guess this is the curse of trying to escape your fate in a weird, almost-developed-but-not-quite country which you cannot completely leave behind, but also cannot come back to for a safe and secure life.&lt;/p&gt;
&lt;p&gt;I don’t know why I’m publishing this. May be I’m tired&lt;sup&gt;&lt;a href=&quot;#user-content-fn-4&quot; id=&quot;user-content-fnref-4&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; or may be it was just a long writing exercise to share one of my favorite songs, “Roots” by Alice Merton:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I like standin’ still, but that’s just a wishful plan
Ask me where I come from, I’ll say a different land
But I’ve got memories and travel like gypsies in the night&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;I’ve got no roots, but my home was never on the ground&lt;/p&gt;
&lt;/blockquote&gt;
&lt;iframe style=&quot;border-radius:12px&quot; src=&quot;https://open.spotify.com/embed/track/7DFSnCBYlus7i4U9KKlbdU?utm_source=generator&quot; width=&quot;100%&quot; height=&quot;352&quot; frameBorder=&quot;0&quot; allow=&quot;autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture&quot; loading=&quot;lazy&quot;&gt;&lt;/iframe&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;I asked my mum whether we were taking the sink with us too. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;Yes, I took a girl to the local movie theater with a romantic walk when I was 8. It was the talk of the town — at least between 8-year-olds. &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;Yes some of you have the smarts to do it quicker, and &lt;em&gt;some&lt;/em&gt; people take longer. Topic of another story. &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-4&quot;&gt;
&lt;p&gt;Just checked, yes I am tired. &lt;a href=&quot;#user-content-fnref-4&quot; data-footnote-backref aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item><item><title>Life Lessons from a Rotary Encoder</title><link>https://byk.im/posts/life-lessons-from-a-rotary-encoder</link><guid isPermaLink="true">https://byk.im/posts/life-lessons-from-a-rotary-encoder</guid><description>When you have eliminated the impossible, whatever remains, however improbable, must be the truth.</description><pubDate>Thu, 24 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recently I got back into an archaic pastime activity of mine: working on hobby electronics. I already had a breadboard lying around but since I am lazy I wanted an Arduino board, complete with all the things I may possibly need: push buttons, LEDs, a 7-segment display, a dot-matrix LCD etc. I bought a &lt;a href=&quot;https://www.robotistan.com/tinylab-exclusive-kit&quot;&gt;TinyLab&lt;/a&gt; experiment board based on the recommendation from my poorer self &lt;a href=&quot;https://www.facebook.com/madBYK/posts/10154533891557907&quot;&gt;from 6 years ago on Facebook&lt;/a&gt;. I know Facebook is an evil machine forced upon us by our alien overlords but it has a tender side surfacing ancient wisdom via its memories feature. Anyway, one of the crucial and interesting components on the board was the rotary encoder. With its infinite, tactile rotation and the new-found popularity among the mechanical keyboard community, this little knob quickly became my new obsession. Trying to read from what’s passing through its contacts would lead to profound realizations about life, universe, and everything — moving our understanding of 42, an inch forward.&lt;/p&gt;
&lt;p&gt;My rotary encoder has 5 contacts: VCC, ground, Phase A, Phase B, and push button. It is of the mechanical type. A mechanical encoder means there is mechanical contact between these A and B pins and the rotating disk inside. It is a lot like a pair of buttons being smashed in perfect order tens of times per second. The most interesting part is the rotation direction detection which is roughly done by figuring out which of these buttons get smashed first. The reason these pins are called “phase” contacts is because when the encoder is rotated, you get a square wave out of them which are out of phase by 90 degrees. This is so that you can determine the direction of rotation based on whichever is ahead of the other.&lt;/p&gt;
&lt;a href=&quot;https://en.wikipedia.org/wiki/File:Quadrature_Diagram.svg&quot;&gt;&lt;img src=&quot;https://byk.im/_astro/rotary-encoder-pulse.CSTkyaRq_Z2k7gfK.webp&quot; alt=&quot;Two square waves in quadrature. Drawn to match the Gray code chart in Rotary encoder. In this diagram, clockwise rotation is towards the right, and counter-clockwise to the left.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;750&quot; height=&quot;250&quot; class=&quot;dark:invert&quot;&gt;&lt;/a&gt;
&lt;p&gt;As a self-thought programmer who’s been writing code for 24 years on “ideal” computers, I didn’t even bother to learn much of the above at first. For me, this was simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;there are two buttons&lt;/li&gt;
&lt;li&gt;in each loop you read the values of these&lt;/li&gt;
&lt;li&gt;if both are 0 = no rotation&lt;/li&gt;
&lt;li&gt;if one is 1 and the other is 0 = encoder is turning in the direction of the pin that is on&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Oh the arrogance even at this ripe age of 33! I was so wrong that it took me a whopping 3 days to reliably read this tiny little marvel of electromechanics. Let’s start with the “obvious” issues:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reading 0 from both pins does not mean we are stopping&lt;/li&gt;
&lt;li&gt;It is possible to read 1 from both pins and the one being 1 does not dictate the direction by its own&lt;/li&gt;
&lt;li&gt;If you read the values in each loop, you may actually miss values as they don’t sit there and wait for you to read. Life goes on, the encoder keeps rotating, and if your main loop is slow you miss your window of opportunity&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So that’s 3 out of 4 from my initial assumptions. At least, I was right about there being two buttons. Sort of.&lt;/p&gt;
&lt;p&gt;The solutions were simple but profound. For timing, you prioritize reading rotary encoder inputs by using pin interrupts. An interrupt is a special instruction in micro-controllers that tell them to drop whatever they are doing and attend a special task. It is a lot like when your kid starts screaming: you drop whatever you’re doing and immediately &lt;del&gt;shush&lt;/del&gt; soothe her. Luckily, rotary encoders are less demanding than toddlers: they just want to be heard (well, maybe kids want &lt;em&gt;just that&lt;/em&gt; too?). So we read and store the state of these A and B pins when there’s a change. Ideally we’d set up the interrupts on both pins but due to the wiring of my board, and some limitations of the Atmega 32u4, I could only listen to one pin. This is very much like hearing only from one of your ears as the other one is gone due to the earlier screaming. Not terrible but you just have to accept the fact that you may not register the initial click in one direction. Again, definitely much less worse than losing your sense of sound directionality along with your will to live after getting yelled at your right ear just because you shut the water faucet off that has been running for the past 187 seconds for the entertainment of a little human’s growing mind.&lt;/p&gt;
&lt;a href=&quot;https://en.wikipedia.org/wiki/File:Incremental_directional_encoder.gif&quot;&gt;&lt;img src=&quot;https://byk.im/_astro/incremental-directional-rotary-encoder.CHtKjIOi_AgQwV.webp&quot; alt=&quot;Example of two-row rotary encoder for speed and direction detection. Basically a 2-bit Gray code pattern.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;750&quot; height=&quot;436&quot; class=&quot;dark:invert&quot;&gt;&lt;/a&gt;
&lt;p&gt;The solution to detecting the direction of a “click” is also simple: just do some book keeping. Record which pin got triggered first, then on the next cycle compare its value with the new state. For instance if you saw A go high (meaning it switched from 0 to 1), while B is 0 you are rotating in one direction (phase of A is ahead of B). If B was 1 while A was going high, that means it is rotating in the other direction as phase of B is ahead of A (or they are playing a weird version of beer pong). Since this is a bunch of if statements and some variables, I wrote it up and tested quickly. I surely was able to read every single click on the encoder, that said the direction was completely unstable. Just like a toddler learning to ride a scooter, the direction was flipping like crazy. This made no sense at all (except for toddlers and scooters). Computer chips and solid metal disks listen to reason unlike 2-year-old human beings!&lt;/p&gt;
&lt;p&gt;I tried blaming the compiler gods but they were too busy torturing my boxed copy who is learning Rust from a borrowed future memory segment&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. Thus, I seized this rare moment where I got to put my computer science knowledge to work. I was going to build a state machine as &lt;a href=&quot;https://readwise.io/reader/shared/01jfx8n0nkgmnsjk9zmx31y2zj&quot;&gt;one wise blog post&lt;/a&gt; suggested. The idea is deceptively simple: not all states you can read from the pins are valid states. For instance, if you look at the wave picture above, you’d see that when you are turning in one direction, you should never see pin A from going low to high while B is 0. So you construct a state table, listing all states and valid state transitions you may accept, and ignore everything else. Doing this fixed all the weird direction jumps! The little cost I paid was some skipped clicks very occasionally but that’s a little price to pay for stability.&lt;/p&gt;
&lt;img src=&quot;https://byk.im/_astro/lila-rotary-encoder-twist.CgVQ6B_j_2pO82K.webp&quot; alt=&quot;Short clip of a rotary encoder being used to control a number on a 7-segment display.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; width=&quot;750&quot; height=&quot;451&quot;&gt;
&lt;p&gt;Although this was a success, I was simply wondering why I had to use actual math and science and how these invalid state transitions could happen in the first place. After some googling, it finally hit me: nothing is ideal, especially mechanical contacts! We can model them and show diagrams like the ones above as their idealized approximations but real world is just messy. It was simply the stuttering of these imperfect copper contacts, sending hysterical signals to my code which expected a perfect square wave. No wonder why it was confused.&lt;/p&gt;
&lt;p&gt;In the end, I was quite surprised by getting smacked in the face by a rotary encoder with bitter truths about life:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;timing and catching your window of opportunity is very important&lt;/li&gt;
&lt;li&gt;life is just messy, no matter how much you try to smooth it out&lt;/li&gt;
&lt;/ul&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Rust_%28programming_language%29&quot;&gt;Rust&lt;/a&gt; is a newish programming language that has a notoriously high learning-curve but provides great memory safety. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded></item></channel></rss>