<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Burak Yigit Kaya</title>
    <description>The latest articles on DEV Community by Burak Yigit Kaya (@byk).</description>
    <link>https://dev.to/byk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F494298%2F49ebb102-fc66-41d2-b02f-97625b5eb823.png</url>
      <title>DEV Community: Burak Yigit Kaya</title>
      <link>https://dev.to/byk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/byk"/>
    <language>en</language>
    <item>
      <title>Marking it Up (and Down)</title>
      <dc:creator>Burak Yigit Kaya</dc:creator>
      <pubDate>Wed, 02 Jul 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/byk/marking-it-up-and-down-2p8l</link>
      <guid>https://dev.to/byk/marking-it-up-and-down-2p8l</guid>
      <description>&lt;h2&gt;
  
  
  First, there was plain text
&lt;/h2&gt;

&lt;p&gt;When I first learned about &lt;a href="https://daringfireball.net/projects/markdown/" rel="noopener noreferrer"&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="https://faq.whatsapp.com/539178204879377/?cms_platform=web&amp;amp;locale=en_US" rel="noopener noreferrer"&gt;including WhatsApp&lt;/a&gt; adopted it.&lt;/p&gt;

&lt;h2&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="https://github.com/microsoft/playwright-mcp" rel="noopener noreferrer"&gt;Playwright MCP&lt;/a&gt; but it’s slow and resource-intensive. These issues lead to the creation of services like &lt;a href="https://www.firecrawl.dev/" rel="noopener noreferrer"&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="https://cra.mr/" rel="noopener noreferrer"&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;1&lt;/sup&gt;. I wanted to write a bit about these too but &lt;a href="https://blog.scottlogic.com/ceberhardt/" rel="noopener noreferrer"&gt;Colin Eberhardt&lt;/a&gt; already did a great job with &lt;a href="https://blog.scottlogic.com/2023/05/04/langchain-mini.html" rel="noopener noreferrer"&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&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="https://docs.sentry.io/" rel="noopener noreferrer"&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="https://github.com/rehypejs/rehype-remark" rel="noopener noreferrer"&gt;rehype-remark&lt;/a&gt; package which is a part of the &lt;a href="https://unifiedjs.com/" rel="noopener noreferrer"&gt;unified&lt;/a&gt; project. I was already quite familiar with unified and &lt;a href="https://github.com/remarkjs/remark" rel="noopener noreferrer"&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="https://gist.github.com/BYK/d8b9bdba5d1ea9bc12fdfb2157d93854" rel="noopener noreferrer"&gt;The code&lt;/a&gt; was also simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import type { Root, Heading } from "mdast";
import rehypeParse from "rehype-parse";
import rehypeRemark from "rehype-remark";
import remarkStringify from "remark-stringify";
import { unified } from "unified";

function extractMDSection({ section }: { section?: RegExp }) {
  return (tree: Root) =&amp;gt; {
    const headingIdx = tree.children.findIndex((node) =&amp;gt; {
      return (
        node.type === "heading" &amp;amp;&amp;amp;
        node.children[0] &amp;amp;&amp;amp;
        node.children[0].type === "link" &amp;amp;&amp;amp;
        section?.test(node.children[0].url)
      );
    });
    const heading = tree.children[headingIdx] as Heading;
    const nextHeadingIdx = tree.children.findIndex(
      (node, idx) =&amp;gt;
        idx &amp;gt; headingIdx &amp;amp;&amp;amp;
        node.type === "heading" &amp;amp;&amp;amp;
        node.depth === heading.depth
    );
    tree.children = tree.children.slice(
      headingIdx,
      nextHeadingIdx === -1 ? undefined : nextHeadingIdx
    );

    return tree;
  };
}

export const getWebpageAsMarkdown = async (url: string, section?: RegExp) =&amp;gt; {
  const response = await fetch(url);
  const text = await response.text();
  return String(
    await unified()
      .use(rehypeParse)
      .use(rehypeRemark)
      .use(extractMDSection, { section })
      .use(remarkStringify)
      .process(text)
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Putting it everywhere, and fast
&lt;/h2&gt;

&lt;p&gt;Then &lt;a href="https://llmstxt.org/" rel="noopener noreferrer"&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;2&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="https://github.com/getsentry/sentry-docs/pull/13994.patch" rel="noopener noreferrer"&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="https://github.com/codyde" rel="noopener noreferrer"&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="https://nodejs.org/api/worker_threads.html" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// On a 16-core machine, 8 workers were optimal (and slightly faster than 16)
const numWorkers = Math.max(Math.floor(cpus().length / 2), 2);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const text = (await readFile(source, { encoding: "utf8" }))
  // Remove all script tags, as they are not needed in markdown
  // and they are not stable across builds, causing cache misses
  .replace(/&amp;lt;script[^&amp;gt;]*&amp;gt;[\s\S]*?&amp;lt;\/script&amp;gt;/gi, "");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="https://x.com/zeeg/status/1938619824751653303" rel="noopener noreferrer"&gt;David started using for a much better search experience&lt;/a&gt;.&lt;/p&gt;

&lt;h2&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="https://vercel.com/docs/functions/runtimes/node-js" rel="noopener noreferrer"&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 as with the top level &lt;code&gt;Promise.all&lt;/code&gt;, I was creating all 8600+ promises all at once, which themselves triggered more open file handlers. Again, obviously this is insane so another old friend, &lt;a href="https://github.com/sindresorhus/p-limit" rel="noopener noreferrer"&gt;&lt;code&gt;p-limit&lt;/code&gt;&lt;/a&gt;&lt;sup&gt;3&lt;/sup&gt;, came to the rescue. 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="https://github.com/kentcdodds/mdx-bundler" rel="noopener noreferrer"&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;4&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&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="https://github.com/getsentry/sentry-docs/pull/13994" rel="noopener noreferrer"&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="https://github.com/getsentry/sentry-docs/pull/14096" rel="noopener noreferrer"&gt;ci(md): Add caching to md-export script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-docs/pull/14109" rel="noopener noreferrer"&gt;ci(build): Parallelize and cache mdx pipeline - fix md cache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-docs/pull/14171" rel="noopener noreferrer"&gt;ci(md): Upload md files to R2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/getsentry/sentry-docs/pull/14193" rel="noopener noreferrer"&gt;feat(md): Use page title as the top level title&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/getsentry/sentry-docs/pull/14196" rel="noopener noreferrer"&gt;feat(md): Rewrite URLs to be absolute and to .md versions&lt;/a&gt;
&lt;h2 id="footnote-label"&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id="user-content-fn-1"&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;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-2"&gt;
&lt;p&gt;Yup, let’s cringe together. ↩&lt;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-3"&gt;
&lt;p&gt;Btw, I still refuse to believe &lt;a href="https://github.com/sindresorhus" rel="noopener noreferrer"&gt;Sindre Sorhus&lt;/a&gt; is a real person. That is alien-level productivity and reach 🙇🏻‍♂️ ↩&lt;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-4"&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;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>markdown</category>
      <category>llm</category>
    </item>
    <item>
      <title>Nightmare on Apple Street</title>
      <dc:creator>Burak Yigit Kaya</dc:creator>
      <pubDate>Thu, 08 May 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/byk/nightmare-on-apple-street-2apc</link>
      <guid>https://dev.to/byk/nightmare-on-apple-street-2apc</guid>
      <description>&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="https://dev.to/posts/fossilize#a-wild-boss-appears-signing-and-notarizing-on-macos"&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&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="https://developer.apple.com/programs/whats-included/" rel="noopener noreferrer"&gt;set you back $99&lt;/a&gt;&lt;sup&gt;1&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;2&lt;/sup&gt;. If you are trying to enroll your organization, god help you: you need your &lt;a href="https://developer.apple.com/help/account/membership/D-U-N-S/" rel="noopener noreferrer"&gt;D-U-N-S number&lt;/a&gt;.&lt;sup&gt;3&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="https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle" rel="noopener noreferrer"&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&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;4&lt;/sup&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/indygreg/apple-platform-rs/releases/latest" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openssl genrsa -out private.pem 2048
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rcodesign generate-certificate-signing-request --pem-file private.pem --csr-pem-file csr.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="https://developer.apple.com/account/resources/certificates/add" rel="noopener noreferrer"&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;/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;/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;/li&gt;
&lt;li&gt;&lt;p&gt;Before finishing, we need to note down your Team ID:&lt;/p&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&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="https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api" rel="noopener noreferrer"&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="https://appstoreconnect.apple.com/access/integrations/api" rel="noopener noreferrer"&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;/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&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="https://github.com/BYK/fossilize/blob/main/entitlements.plist" rel="noopener noreferrer"&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="https://developer.apple.com/documentation/bundleresources/entitlements" rel="noopener noreferrer"&gt;the vast array of entitlements&lt;/a&gt;that Apple offers. There’s also &lt;a href="https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution" rel="noopener noreferrer"&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="https://developer.apple.com/documentation/security/resolving-common-notarization-issues#Ensure-properly-formatted-entitlements" rel="noopener noreferrer"&gt;properly formatted&lt;/a&gt;&lt;sup&gt;5&lt;/sup&gt; next to the binary you want to sign and notarize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sign here please&lt;sup&gt;6&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rcodesign sign --team-name &amp;lt;your_team_id&amp;gt; --p12-file codesign.p12 --for-notarization -e entitlements.plist &amp;lt;your_binary_path&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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&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;7&lt;/sup&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;zip app.zip &amp;lt;path_to_your_app&amp;gt;
rcodesign notary-submit --api-key-file codesign_key.json --wait app.zip
rm app.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&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="https://github.com/getsentry/spotlight/blob/4f3e34a43e5d1949f664fc8ea88f84b1050274af/.github/workflows/build.yml#L61-L65" rel="noopener noreferrer"&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="https://github.com/getsentry/spotlight/blob/4f3e34a43e5d1949f664fc8ea88f84b1050274af/.github/workflows/build.yml#L96-L101" rel="noopener noreferrer"&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&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;I’ve used the excellent docs &lt;a href="https://gregoryszorc.com/" rel="noopener noreferrer"&gt;Gregory Szorc&lt;/a&gt; created for his amazing &lt;a href="https://gregoryszorc.com/docs/apple-codesign" rel="noopener noreferrer"&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="https://gregoryszorc.com/docs/apple-codesign/main/apple_codesign_certificate_management.html" rel="noopener noreferrer"&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="https://gregoryszorc.com/docs/apple-codesign/main/apple_codesign_getting_started.html" rel="noopener noreferrer"&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="https://gist.github.com/karnauskas/f76ab849224f22fc32961288266094a2" rel="noopener noreferrer"&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="https://github.com/karnauskas" rel="noopener noreferrer"&gt;karnauskas&lt;/a&gt; and used parts of it.&lt;/p&gt;

&lt;p&gt;Finally, I’ve used &lt;a href="https://stackoverflow.com/a/27497899/90297" rel="noopener noreferrer"&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&gt;
  
  
  Thanks
&lt;/h2&gt;

&lt;p&gt;I’d like to thank my colleague &lt;a href="https://github.com/szokeasaurusrex" rel="noopener noreferrer"&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;h2 id="footnote-label"&gt;Footnotes&lt;/h2&gt;

&lt;ol&gt;
&lt;li id="user-content-fn-1"&gt;
&lt;p&gt;You need to scroll all the way to the bottom to see this very unimportant detail. ↩&lt;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-2"&gt;
&lt;p&gt;Yep, I’m being snarky. ↩&lt;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-3"&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;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-4"&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;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-5"&gt;
&lt;p&gt;Being a bit picky, are we dear Apple? ↩&lt;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-6"&gt;
&lt;p&gt;and here, and here, and here, and here… ↩&lt;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-7"&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;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
    </item>
    <item>
      <title>Making your Node.js application last centuries</title>
      <dc:creator>Burak Yigit Kaya</dc:creator>
      <pubDate>Wed, 12 Feb 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/byk/making-your-nodejs-application-last-centuries-ij3</link>
      <guid>https://dev.to/byk/making-your-nodejs-application-last-centuries-ij3</guid>
      <description>&lt;p&gt;I’ve been working on &lt;a href="https://spotlightjs.com/" rel="noopener noreferrer"&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&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;1&lt;/sup&gt;&lt;/p&gt;

&lt;h2&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 NodeJS 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="https://github.com/nexe/nexe" rel="noopener noreferrer"&gt;nexe&lt;/a&gt; and I was about to give it a shot when I noticed this “&lt;a href="https://nodejs.org/api/single-executable-applications.html#single-executable-applications" rel="noopener noreferrer"&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&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="https://classic.yarnpkg.com/lang/en/" rel="noopener noreferrer"&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;2&lt;/sup&gt;. This was using a bundler such as Webpack (remember, this is 2015). I then used this technique on &lt;a href="https://github.com/getsentry/craft/" rel="noopener noreferrer"&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="https://esbuild.github.io/" rel="noopener noreferrer"&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="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta" rel="noopener noreferrer"&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;3&lt;/sup&gt;, I was &lt;a href="https://github.com/evanw/esbuild/issues/1492" rel="noopener noreferrer"&gt;not the first person to bump into this&lt;/a&gt; and there was &lt;a href="https://github.com/evanw/esbuild/issues/1492#issuecomment-893144483" rel="noopener noreferrer"&gt;a simple yet crude solution&lt;/a&gt; that I’d happily take.&lt;/p&gt;

&lt;h2&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&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;4&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;5&lt;/sup&gt;. I also remembered the ambitious project from &lt;a href="https://gregoryszorc.com/" rel="noopener noreferrer"&gt;indygreg&lt;/a&gt;, opening Apple’s code signing black box to the masses and to other platforms: &lt;a href="https://github.com/indygreg/apple-platform-rs/" rel="noopener noreferrer"&gt;apple-platform-rs&lt;/a&gt;. Now, with the power of &lt;code&gt;rcodesign&lt;/code&gt;, I could sign and notarize by 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&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="https://github.com/getsentry/spotlight/blob/bb7a499e5f95db84bab3c2929762ddc87cf36350/packages/spotlight/build.js" rel="noopener noreferrer"&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="https://github.com/BYK/fossilize" rel="noopener noreferrer"&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="https://github.com/nodejs/postject" rel="noopener noreferrer"&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;h2 id="footnote-label"&gt;Footnotes&lt;/h2&gt;

&lt;ol&gt;
&lt;li id="user-content-fn-1"&gt;
&lt;p&gt;Yet I happily use VS Code and Slack. Oh the hypocrisy! ↩&lt;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-2"&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="https://nodejs.org/api/single-executable-applications.html#v8-code-cache-support" rel="noopener noreferrer"&gt;supports this&lt;/a&gt;. ↩&lt;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-3"&gt;
&lt;p&gt;Or unfortunately, depending on how you look at it. ↩&lt;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-4"&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;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-5"&gt;
&lt;p&gt;A blog post dedicated to this journey is being written as of this writing. ↩&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>node</category>
      <category>binary</category>
      <category>javascript</category>
      <category>packaging</category>
    </item>
    <item>
      <title>Docker Volume Caching on GitHub Actions</title>
      <dc:creator>Burak Yigit Kaya</dc:creator>
      <pubDate>Tue, 14 Jan 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/byk/docker-volume-caching-on-github-actions-542</link>
      <guid>https://dev.to/byk/docker-volume-caching-on-github-actions-542</guid>
      <description>&lt;p&gt;I joined Sentry to exclusively work on their &lt;a href="https://github.com/getsentry/self-hosted" rel="noopener noreferrer"&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="https://github.com/getsentry/snuba" rel="noopener noreferrer"&gt;Snuba&lt;/a&gt; and along with that Kafka, &lt;a href="https://github.com/getsentry/relay" rel="noopener noreferrer"&gt;Relay&lt;/a&gt;, &lt;a href="https://github.com/getsentry/symbolicator" rel="noopener noreferrer"&gt;Symbolicator&lt;/a&gt; and others. Because it was supposed to be simple, self-hosted (or &lt;a href="https://github.com/getsentry/onpremise/" rel="noopener noreferrer"&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="https://github.com/actions/cache/" rel="noopener noreferrer"&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="https://stackoverflow.com/" rel="noopener noreferrer"&gt;StackOverflow&lt;/a&gt; (yeah, still not on that “ChatGPT for everything” bandwagon&lt;sup&gt;1&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;2&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;3&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="https://github.com/actions/toolkit/issues/946" rel="noopener noreferrer"&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&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo cp /usr/bin/tar /usr/bin/tar.orig
sudo echo 'exec tar.orig --numeric-owner -p --same-owner "$@"' &amp;gt; /usr/bin/tar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo cp /usr/bin/tar /usr/bin/tar.orig
echo 'exec /usr/bin/tar.orig --numeric-owner -p --same-owner "$@"' | sudo tee /usr/bin/tar &amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;4&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbyk.im%2F_astro%2Ffork-bomb.BKc6HNhp_18ewLI.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbyk.im%2F_astro%2Ffork-bomb.BKc6HNhp_18ewLI.webp" alt="A smiling bomb with a fork stuck to it." width="640" height="640"&gt;&lt;/a&gt;&lt;/p&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&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="https://github.com/BYK/docker-volume-cache-action" rel="noopener noreferrer"&gt;BYK/docker-volume-cache-action&lt;/a&gt; and cache responsibly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbyk.im%2F_astro%2Fci-minutes-saved.CL2blTQQ_1S4WAP.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbyk.im%2F_astro%2Fci-minutes-saved.CL2blTQQ_1S4WAP.webp" alt="A repeated CI run which took about 13 minutes versus 16 minutes without the cache." width="750" height="462"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="footnote-label"&gt;Footnotes&lt;/h2&gt;

&lt;ol&gt;
&lt;li id="user-content-fn-1"&gt;
&lt;p&gt;That said all images for this article was generated by &lt;a href="https://deepai.org/machine-learning-model/text2img" rel="noopener noreferrer"&gt;DeepAI Image Generator&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-2"&gt;
&lt;p&gt;Looking at you &lt;a href="https://hub.docker.com/r/confluentinc/cp-kafka" rel="noopener noreferrer"&gt;confluentinc/cp-kafka&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-3"&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;/p&gt;
&lt;/li&gt;
&lt;li id="user-content-fn-4"&gt;
&lt;p&gt;Me when I realized this: mother forking shirt balls! ↩&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;



</description>
    </item>
    <item>
      <title>Having a Good Ol' RSS Feed in Astro</title>
      <dc:creator>Burak Yigit Kaya</dc:creator>
      <pubDate>Mon, 30 Dec 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/byk/having-a-good-ol-rss-feed-in-astro-4lfm</link>
      <guid>https://dev.to/byk/having-a-good-ol-rss-feed-in-astro-4lfm</guid>
      <description>&lt;p&gt;After reviving this blog with &lt;a href="https://astro.build" rel="noopener noreferrer"&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="https://github.com/palmiak/pacamara-astro/blob/cbff90909afbf4fa08fdfd47c860d4c732b00330/src/pages/rss.xml.js#L5" rel="noopener noreferrer"&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="https://pacamara-astro-6y7xr.kinsta.page/" rel="noopener noreferrer"&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;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const MAX_ITEMS = 10;

const posts = (await getCollection("posts"))
  .sort(descDateSort)
  .slice(0, MAX_ITEMS);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="https://docs.astro.build/en/guides/endpoints/#static-file-endpoints" rel="noopener noreferrer"&gt;“endpoint”&lt;/a&gt; and was not able to render Astro components. Even &lt;a href="https://docs.astro.build/en/recipes/rss/" rel="noopener noreferrer"&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="https://scottwillsey.com/rss-pt2/" rel="noopener noreferrer"&gt;very creative solution&lt;/a&gt;, I bumped into a more straightforward one first: &lt;a href="https://blog.damato.design/posts/astro-rss-mdx/" rel="noopener noreferrer"&gt;https://blog.damato.design/posts/astro-rss-mdx/&lt;/a&gt;. This solution uses the new and experimental &lt;a href="https://docs.astro.build/en/reference/container-reference/" rel="noopener noreferrer"&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="https://github.com/BYK/byk.github.io/commit/0449bf42da53a98f72dbafe5e915fa6a4f530eba" rel="noopener noreferrer"&gt;Committed&lt;/a&gt;, pushed, and &lt;a href="https://github.com/BYK/byk.github.io/actions/runs/12537931496/job/34962544900" rel="noopener noreferrer"&gt;got yelled at by GitHub Actions&lt;/a&gt; with the following cryptic error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cannot test case insensitive FS, CLIENT_ENTRY does not point to an existing file: /home/runner/work/byk.github.io/byk.github.io/dist/client/client.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="https://github.com/withastro/roadmap/pull/916#issuecomment-2256059117" rel="noopener noreferrer"&gt;https://github.com/withastro/roadmap/pull/916#issuecomment-2256059117&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// astro.config.mjs -- add the following
{
  vite: {
    ssr: {
      external: ['astro/container', '@astrojs/mdx'],
    },
  },
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="https://readwise.io/i/burak13" rel="noopener noreferrer"&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="https://github.com/withastro/astro/blob/ebe2aa95c7f4a6559cec8b82d155da34a57bdd53/packages/astro/src/assets/services/service.ts#L370" rel="noopener noreferrer"&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: '...'}&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="https://docs.astro.build/en/reference/configuration-reference/#buildassetsprefix" rel="noopener noreferrer"&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="///rss.xml"&gt;subscribe to my blog&lt;/a&gt;, now you can 😏&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>astro</category>
      <category>rss</category>
    </item>
    <item>
      <title>Life Lessons from a Rotary Encoder</title>
      <dc:creator>Burak Yigit Kaya</dc:creator>
      <pubDate>Thu, 24 Mar 2022 00:00:00 +0000</pubDate>
      <link>https://dev.to/byk/life-lessons-from-a-rotary-encoder-1agl</link>
      <guid>https://dev.to/byk/life-lessons-from-a-rotary-encoder-1agl</guid>
      <description>&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="https://www.robotistan.com/tinylab-exclusive-kit" rel="noopener noreferrer"&gt;TinyLab&lt;/a&gt; experiment board based on the recommendation from my poorer self &lt;a href="https://www.facebook.com/madBYK/posts/10154533891557907" rel="noopener noreferrer"&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;p&gt;&lt;a href="https://en.wikipedia.org/wiki/File:Quadrature_Diagram.svg" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbyk.im%2F_astro%2Frotary-encoder-pulse.CSTkyaRq_Z16qqq.webp" alt="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." width="750" height="250"&gt;&lt;/a&gt;&lt;/p&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;p&gt;&lt;a href="https://en.wikipedia.org/wiki/File:Incremental_directional_encoder.gif" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbyk.im%2F_astro%2Fincremental-directional-rotary-encoder.CHtKjIOi_2t5zFA.webp" alt="Example of two-row rotary encoder for speed and direction detection. Basically a 2-bit Gray code pattern." width="750" height="436"&gt;&lt;/a&gt;&lt;/p&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;1&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="https://readwise.io/reader/shared/01jfx8n0nkgmnsjk9zmx31y2zj" rel="noopener noreferrer"&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;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbyk.im%2F_astro%2Flila-rotary-encoder-twist.CgVQ6B_j_Z1NjoeX.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbyk.im%2F_astro%2Flila-rotary-encoder-twist.CgVQ6B_j_Z1NjoeX.webp" alt="Short clip of a rotary encoder being used to control a number on a 7-segment display." width="279" height="168"&gt;&lt;/a&gt;&lt;/p&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;h2 id="footnote-label"&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id="user-content-fn-1"&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Rust_%28programming_language%29" rel="noopener noreferrer"&gt;Rust&lt;/a&gt; is a newish programming language that has a notoriously high learning-curve but provides great memory safety. ↩&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>arduino</category>
      <category>electronics</category>
      <category>encoder</category>
    </item>
    <item>
      <title>The "Improbable" Truth: Rare Browser Bugs</title>
      <dc:creator>Burak Yigit Kaya</dc:creator>
      <pubDate>Thu, 13 Dec 2012 00:00:00 +0000</pubDate>
      <link>https://dev.to/byk/the-improbable-truth-rare-browser-bugs-ee3</link>
      <guid>https://dev.to/byk/the-improbable-truth-rare-browser-bugs-ee3</guid>
      <description>&lt;p&gt;Over the last two weeks at &lt;a href="http://disqus.com" rel="noopener noreferrer"&gt;Disqus&lt;/a&gt; we discovered two annoying browser bugs. Both were only happening on iOS, which reminded me the famous quote from &lt;a href="http://en.wikipedia.org/wiki/Sherlock_holmes" rel="noopener noreferrer"&gt;Sherlock Holmes&lt;/a&gt;: “when you have eliminated the impossible, whatever remains, however improbable, must be the truth”.&lt;/p&gt;

&lt;p&gt;The first one was a bad one. We had reports from users not being able to login to our system from the embedded commenting widget, but only on iOS. The symptoms were even stranger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users were shown the login page in a popup&lt;/li&gt;
&lt;li&gt;Users could login and the cookie was set&lt;/li&gt;
&lt;li&gt;The popup stayed open but notified the embed about the logged in user&lt;/li&gt;
&lt;li&gt;The embed did not recognize the logged in user until after a refresh&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We started debugging and discovered that the embed actually makes a call to get details of the logged in user after it gets the notification from the popup. However, the result of the request was never processed. More so, if you refreshed the popup and then closed it, it worked!&lt;/p&gt;

&lt;p&gt;We spent many hours in the iPhone emulator, mostly due to the cumbersome nature of all the emulators, we discovered that if you ever opened a popup and held a reference to it, iOS suspends the events on the parent page but still executes some of the code. So, in our case, the XHR call was being made and the response was received, but the callback was not called. The cause of our bug reports was this callback being responsible for closing the login popup. The embed was dead-locked for waiting for the request to finish to close the popup and the browser was waiting for the popup to close to fire the callback. As you can imagine, there were no visible reports of this behavior, anywhere on the internets.&lt;/p&gt;

&lt;p&gt;The other one was even weirder: we were seeing a certain content being repeated exactly three times whereas it should have appended to the DOM only once. The intermittent nature of the problem suggested a hard-to-track race condition but that turned out not to be the case. We were able to mitigate the problem easily by emptying the parent element before appending the content. This was only a symptomatic cure though so we proceeded on our adventure to find the root cause.&lt;/p&gt;

&lt;p&gt;After many hours of debugging, which was also very cumbersome due to the specific “ritual” to reproduce it consistently, we traced the problem to the XHR success callback getting called 3 times with &lt;code&gt;readyState == 4&lt;/code&gt; instead of only once. The new information suggested, surprise, a race condition, but tracking the number of XHR objects eliminated that possibility entirely.&lt;/p&gt;

&lt;p&gt;It turns out that the &lt;code&gt;onreadystatechange&lt;/code&gt; event was getting fired even though&lt;code&gt;readyState&lt;/code&gt; did not change at all. Not surprisingly, since this was happening at the &lt;strong&gt;completed&lt;/strong&gt; state, the callback was fired multiple times for the same request. A Google search revealed another poor soul who encountered the same issue:&lt;a href="https://github.com/madrobby/zepto/pull/633" rel="noopener noreferrer"&gt;https://github.com/madrobby/zepto/pull/633&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There it was: a weird, hard-to-reproduce browser bug breaking our product randomly. After realizing this fact, we went ahead and did what we had to do:&lt;a href="https://github.com/ded/reqwest/pull/93" rel="noopener noreferrer"&gt;patched Reqwest&lt;/a&gt;, the XHR library we use at Disqus.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>vintage</category>
    </item>
  </channel>
</rss>
