<?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: Jordan Tukum Folem</title>
    <description>The latest articles on DEV Community by Jordan Tukum Folem (@kiri1101).</description>
    <link>https://dev.to/kiri1101</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4225369%2F6a05e1f4-cc86-456d-95a8-72cd13dcfc72.jpg</url>
      <title>DEV Community: Jordan Tukum Folem</title>
      <link>https://dev.to/kiri1101</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kiri1101"/>
    <language>en</language>
    <item>
      <title>Showing your private GitHub work on your profile, without leaking a single repo name</title>
      <dc:creator>Jordan Tukum Folem</dc:creator>
      <pubDate>Wed, 04 Nov 2026 04:45:22 +0000</pubDate>
      <link>https://dev.to/kiri1101/showing-your-private-github-work-on-your-profile-without-leaking-a-single-repo-name-j0n</link>
      <guid>https://dev.to/kiri1101/showing-your-private-github-work-on-your-profile-without-leaking-a-single-repo-name-j0n</guid>
      <description>&lt;p&gt;Most of what I build as a freelancer lives in private client repositories. That's normal for client work, but it made my GitHub profile look half-empty: the public stats cards counted only the 20% of my work that happened to be public.&lt;/p&gt;

&lt;p&gt;Then the public github-readme-stats instance went down (&lt;a href="https://github.com/anuraghazra/github-readme-stats/issues/3851" rel="noopener noreferrer"&gt;#3851&lt;/a&gt;), and my cards turned into broken-image icons. I took that as the push to build my own: a small Cloudflare Worker that counts private work safely and that I now run for free.&lt;/p&gt;

&lt;p&gt;It's open source: &lt;strong&gt;&lt;a href="https://github.com/kiri1101/github-profile-cards" rel="noopener noreferrer"&gt;kiri1101/github-profile-cards&lt;/a&gt;&lt;/strong&gt; (MIT, with a Deploy to Cloudflare button).&lt;/p&gt;

&lt;h2&gt;
  
  
  The privacy problem: totals yes, names never
&lt;/h2&gt;

&lt;p&gt;The goal was simple to state: show how much private work I do, without revealing &lt;em&gt;what&lt;/em&gt; it is. No repository names, no client names, ever, not on the card and not in logs.&lt;/p&gt;

&lt;p&gt;So the Worker only ever stores and renders &lt;strong&gt;totals&lt;/strong&gt;. Each row on the card shows public, private and total, where private is simply &lt;code&gt;total − public&lt;/code&gt;. The public part comes from counting only contributions in public repositories. The names never leave GitHub's API response; they're summed away immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it needs two tokens
&lt;/h2&gt;

&lt;p&gt;This surprised me. No single GitHub token gives you everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;classic token with only &lt;code&gt;read:user&lt;/code&gt;&lt;/strong&gt; returns your &lt;em&gt;total&lt;/em&gt; commit contributions for the year, private ones included. But the PR and issue counts it can see are public-only.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;fine-grained token&lt;/strong&gt; with read-only &lt;strong&gt;Metadata, Issues and Pull requests&lt;/strong&gt; on all your repositories sees your private PRs and issues. It also sees your repositories' language breakdown.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither token can write anything. The Worker reads which account owns them (&lt;code&gt;query { viewer { login } }&lt;/code&gt;), so there's no username to configure. If the two tokens belong to different accounts, it refuses to refresh rather than mixing two people's numbers on one card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cron + KV: never rate-limited, never "down"
&lt;/h2&gt;

&lt;p&gt;Rendering cards on every request is what makes public instances expensive and fragile. This Worker does the opposite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;cron trigger every 6 hours&lt;/strong&gt; fetches the numbers, validates them, renders light and dark SVGs, and stores them in &lt;strong&gt;Workers KV&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Requests just read KV, which is fast and costs nothing on the free plan. If KV is ever empty or unreachable, you get a placeholder card, never a broken image.&lt;/li&gt;
&lt;li&gt;New data is &lt;strong&gt;validated before it replaces the old&lt;/strong&gt;: a sudden drop (say, a counter falling by more than 10%) is held back until the same drop repeats four times, so a flaky API response can never wipe your card.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A GitHub README trap worth knowing
&lt;/h2&gt;

&lt;p&gt;If you wrap a &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; (the light/dark image switcher) in a link, GitHub's README renderer moves the &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; into its own image link outside your anchor. Clicks then open the raw image, and theme switching breaks. The fix for linked cards: one SVG that themes &lt;em&gt;itself&lt;/em&gt; with &lt;code&gt;@media (prefers-color-scheme: dark)&lt;/code&gt; inside, used as a plain &lt;code&gt;&amp;lt;a&amp;gt;&amp;lt;img&amp;gt;&amp;lt;/a&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests that can actually fail
&lt;/h2&gt;

&lt;p&gt;There's a fair complaint going around that AI-assisted code often comes with tests that always pass, because they were written to match the code rather than the feature. I wanted proof mine weren't like that, so I used &lt;strong&gt;mutation testing&lt;/strong&gt; (&lt;a href="https://stryker-mutator.io/" rel="noopener noreferrer"&gt;StrykerJS&lt;/a&gt;). It plants small bugs in your code (flips a &lt;code&gt;&amp;lt;&lt;/code&gt; to &lt;code&gt;&amp;lt;=&lt;/code&gt;, deletes a call, changes a constant) and checks whether any test notices.&lt;/p&gt;

&lt;p&gt;The first run scored &lt;strong&gt;84%&lt;/strong&gt;: one planted bug in six went unnoticed by a suite that looked fine by coverage. After adding sharp tests whose expected values come from the spec, not from the code's own output, the critical modules (grade formula, validation, privacy split, escaping, refresh safety) score &lt;strong&gt;100%&lt;/strong&gt;. CI fails if it ever drops.&lt;/p&gt;

&lt;p&gt;I also deleted 30 tests that caught none of the planted bugs. Fewer tests, each of which matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Deploy to Cloudflare&lt;/strong&gt; in the &lt;a href="https://github.com/kiri1101/github-profile-cards" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Paste two tokens (the README links straight to GitHub's token pages with the right scopes).&lt;/li&gt;
&lt;li&gt;Embed the two SVG URLs in your profile README.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It runs on the free plan, and an unedited copy shows neutral cards. You can add your own project cards and tech-stack blocks in one config file.&lt;/p&gt;

&lt;p&gt;The grade uses the &lt;code&gt;calculateRank&lt;/code&gt; formula from &lt;a href="https://github.com/anuraghazra/github-readme-stats" rel="noopener noreferrer"&gt;github-readme-stats&lt;/a&gt;, credit where it's due. Issues and ideas are welcome on the repo.&lt;/p&gt;

</description>
      <category>github</category>
      <category>typescript</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
