<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title><![CDATA[By Parker]]></title>
  <link href="https://byparker.com/blog/atom.xml" rel="self"/>
  <link href="https://byparker.com/"/>
  <updated>2026-06-18T18:41:56+00:00</updated>
  <id>https://byparker.com/</id>
  <author>
    <name><![CDATA[parkr]]></name>
  </author>
  <generator uri="https://jekyllrb.com/">Jekyll</generator>

  
  <entry>
    <title type="html"><![CDATA[Transcribe YouTube videos with Whisper.cpp]]></title>
    <link href="https://byparker.com/blog/2024/transcription-of-youtube-video-using-whisper-cpp/"/>
    <updated>2024-01-04T02:32:32+00:00</updated>
    <id>https://byparker.com/blog/2024/transcription-of-youtube-video-using-whisper-cpp</id>
    <content type="html"><![CDATA[<p>I can read more quickly than I can listen. I can search a text file but not
an audio file. There are many cases when the written word is superior to the
spoken word, but sometimes the source data is spoken. Can I extract spoken
word data into something written?</p>

<p><a href="https://github.com/ggerganov/whisper.cpp">Whisper.cpp</a> is an ML model which interprets spoken words and outputs
written words and can be run on a Mac laptop. <a href="https://simonwillison.net/">Simon Willison</a> frequently
refers to his use of MacWhisper on his blog, so I thought I’d try my hand
at a command-line approach to transcribing a YouTube video.</p>

<p>Whisper.cpp is easy to setup but there’s a learning curve. You need:</p>

<ol>
  <li><a href="https://huggingface.co/ggerganov/whisper.cpp/tree/main">ggml-base.en.bin</a> placed in <code class="language-plaintext highlighter-rouge">models/</code> subdirectory</li>
  <li>ggml-metal.metal which is a C++ file</li>
  <li>Your audio in WAV format at 16kHz</li>
</ol>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="c"># Step 1, setup Whisper.cpp</span>
<span class="gp">$</span><span class="w"> </span>brew <span class="nb">install </span>whisper-cpp <span class="c"># or install for your machine type</span>
<span class="gp">$</span><span class="w"> </span><span class="nb">mkdir </span>models
<span class="gp">$</span><span class="w"> </span>wget <span class="nt">-O</span> models/ggml-base.en.bin https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin?download<span class="o">=</span><span class="nb">true</span>
<span class="gp">$</span><span class="w"> </span>wget <span class="nt">-O</span> ggml-metal.metal https://github.com/ggerganov/llama.cpp/raw/master/ggml-metal.metal
</code></pre></div></div>

<p>Now that Whisper.cpp is ready to run, you need to get your input material.
In my case, I got a webm video which had an Opus audio stream. This
required some careful but simple conversion:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="c"># Step 2, get your input</span>
<span class="gp">$</span><span class="w"> </span>youtube-dl <span class="nt">--extract-audio</span> <span class="nt">--audio-format</span> best <span class="o">[</span>yt url]
<span class="gp">$</span><span class="w"> </span>ffmpeg <span class="nt">-i</span> file_from_youtube_dl_execution.opus <span class="nt">-ar</span> 16000 <span class="nt">-vn</span> prepared_input.wav
</code></pre></div></div>

<p>Now you have your input and Whisper.cpp is setup, you can generate your
transcript. You can run it just like <code class="language-plaintext highlighter-rouge">whisper-cpp prepared_input.wav</code> and
copy the output from stdout, or you can pass dictate how it’s output: txt,
csv, srt, vtt, lrc, and json.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span><span class="c"># Step 3, extract your transcript</span>
<span class="gp">$</span><span class="w"> </span>whisper-cpp <span class="nt">--output-txt</span> prepared_input.wav
<span class="gp">$</span><span class="w"> </span>less prepared_input.wav.txt <span class="c"># read your transcript!</span>
</code></pre></div></div>

<p>You can supply as many <code class="language-plaintext highlighter-rouge">--output-&lt;fmt&gt;</code> flags as you would like and it will
output all formats you request. Pretty neat!</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Export your starred episodes in Overcast]]></title>
    <link href="https://byparker.com/blog/2023/exporting-overcast-favorited-episodes/"/>
    <updated>2023-02-15T16:42:00+00:00</updated>
    <id>https://byparker.com/blog/2023/exporting-overcast-favorited-episodes</id>
    <content type="html"><![CDATA[<p><a href="https://overcast.fm">Overcast</a> has been my primary podcast app for many years now. This
years-long history of listening has produced a lot of data, most of which
is lost to time.</p>

<p>Some of it isn’t though. What I have favorited (or starred, or recommended)
is not lost to time. With the release of playlists in Overcast, I could see
all my starred episodes. The data was somewhere! I emailed <a href="https://marco.org">Marco</a>, its
creator and developer, to ask if he could offer some kind of export mechanism
for this from the production app. I also signed up for the TestFlight and
submitted a feature request through the beta app, but I didn’t hear back.</p>

<p>I ruminated on this topic for a few months until a few nights ago. I wanted to
look up a starred episode, but got frustrated scrolling through the UI. I
realized that it would be more efficient to scroll on my Mac, so I opened
Overcast on the Mac. The idea struck me instantly: Marco has mentioned
before that he uses sqlite. Surely I could find the database file on my Mac?</p>

<p>After some hunting, I found it: <code class="language-plaintext highlighter-rouge">oc.db</code> in
<code class="language-plaintext highlighter-rouge">~/Library/Containers/[App UUID]/Data/Documents</code>. I held my breath and opened it
with sqlite3. It worked! Now, how do I find the starred data? Turns out that
Marco has indicated this with a field called
<code class="language-plaintext highlighter-rouge">OCFeedItem.userRecommendedTime</code>. When a user stars an episode, the time is taken and that’s
written to their database and to the servers and used for Overcast’s recommendation engine.</p>

<p>I wrote <a href="https://gist.github.com/parkr/0eb300c93d84365c737c9896a1d97062">some SQL to pull out the episodes</a> and piped it through
Simon Willison’s excellent <a href="https://github.com/simonw/sqlite-utils"><code class="language-plaintext highlighter-rouge">sqlite-utils</code></a>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>sqlite-utils  ~/Library/Containers/.../Data/Documents/oc.db <span class="s2">"</span><span class="si">$(</span>&lt;~/Downloads/overcast_export_recommended_episodes.sql<span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p>:tada: I finally had the data I was looking for. A full list of every episode
that I have ever starred as beautiful JSON, going back to 2017.</p>

<p>I decided to <a href="https://stuff.parkermoore.de/compilations/podcasts.html">publish my favorites</a> since the reason to star them is to
remember them and to share them with the world. You can <a href="https://github.com/parkr/stuff/blob/main/script/import-overcast-podcast-favorites">view the code</a>
if you want to emulate this. If you use my script, consider joining me in
subscribing to Overcast Premium so that Marco can continue to offer Overcast
on the Mac.</p>

<p>I’m extraordinarily excited to finally have access to my data. With the
recent rise of federated social networks and the talk of owning one’s data
(or at least a copy!), I am put at ease that if Overcast went out of
business tomorrow, I would still have a historical record of my favorite
podcast episodes to share with friends and strangers alike.</p>

<p>My thanks to Marco for allowing Overcast to run on the Mac so that I could grab
a copy of my data, and for <a href="https://mastodon.social/@overcastfm/109863328508642286">guiding me through generation of the Overcast.fm permalinks</a></p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Preserve your Instapaper bookmarks, highlights, and notes]]></title>
    <link href="https://byparker.com/blog/2022/archive-instapaper-bookmarks/"/>
    <updated>2022-04-07T17:48:00+00:00</updated>
    <id>https://byparker.com/blog/2022/archive-instapaper-bookmarks</id>
    <content type="html"><![CDATA[<p><em>Do you own your data?</em></p>

<p>I have 3,855 Instapaper bookmarks collected in the over 10 years since I signed up for the service. That’s a lot of reading! The beauty of Instapaper is that it saves a text version of these webpages to its database, allowing you to preserve the original words of the article before it (usually) disappears from the Internet. Instapaper is also a great learning tool, since it allows you to highlight and notate your articles as well.</p>

<p>Instapaper has changed hands a number of times over the years. Initially <a href="https://marco.org/2008/01/28/instapaper">written by Marco Arment</a>, it was <a href="https://marco.org/2013/04/25/instapaper-next-generation">sold to Betaworks in 2013</a> and <a href="https://www.theverge.com/2016/8/23/12595940/instapaper-acquired-pinterest-newsreading-app">sold later to Pinterest in 2016</a>, then <a href="https://techcrunch.com/2018/07/16/instapaper-is-leaving-pinterest-two-years-after-being-acquired/">became its own company</a>. As with any tech product, you don’t know if what they offer today will persist into the future. Apps more famous and well-loved than Instapaper have been shut down over the years, so it’s entirely possible that Instapaper could have the same fate someday. What if Instapaper shut down? Where would that leave me? If I have the full list of all my bookmarks, highlights, and notes, then it would be fine if Instapaper shut down. So, I set out to see if I could make that happen.</p>

<p><a href="https://github.com/parkr/instapaper-archive">parkr/instapaper-archive</a> is the culmination of this work to date. A simple Go program reads from the Instapaper API and from the <code class="language-plaintext highlighter-rouge">instapaper-export.csv</code> file generated from <a href="https://www.instapaper.com/user">user Settings</a> and generates a <a href="https://jekyllrb.com">Jekyll</a> site. (Since the Instapaper API only allows listing up to 500 articles per folder, it was necessary to combine the CSV export with the API results to get a full picture.) It downloads highlights today (<a href="https://github.com/parkr/instapaper-archive/issues/2">not notes yet</a>). It preserves whether an article was favorited (denoted by a star):</p>

<p><img src="/img/2022-04-03-archive-instapaper-bookmarks/screenshot.png" alt="screeenshot of my new instapaper archive" /></p>

<p>But preserving my data is just the start. What could I learn from my archive? I could do all sorts of interesting analysis with it. I can go back and see what I was interested in, say, 2013 and how that compares to my present interests. I can more easily find that one article that shaped my understanding of this or that topic. Most importantly, I own my own data – if Instapaper folded tomorrow, I would not lose a decade’s work.</p>

<p>If you use <code class="language-plaintext highlighter-rouge">instapaper-archive</code>, I’d be interested to hear how your experience goes in an issue on the repo. In the age of closed gardens and locked-up data, it always feels great to truly own your data.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Scanning from the Command Line in a Snap]]></title>
    <link href="https://byparker.com/blog/2022/scanning-from-the-command-line-in-a-snap/"/>
    <updated>2022-03-12T20:57:00+00:00</updated>
    <id>https://byparker.com/blog/2022/scanning-from-the-command-line-in-a-snap</id>
    <content type="html"><![CDATA[<p>Julia Evans recently wrote about <a href="https://jvns.ca/blog/2022/03/08/tiny-programs/">the tiny personal programs she’s
written</a>, one of which was
called “getting my scanner to work better.” She links to <a href="https://gist.github.com/jvns/b5651fd6272eddcd935e5e30874a935f">her wrapper for
<code class="language-plaintext highlighter-rouge">scanimage</code></a>
which got me thinking: is it possible to use <code class="language-plaintext highlighter-rouge">scanimage</code> with my Fujitsu
ScanSnap S1100?</p>

<p>I love this little ScanSnap S1100 for its miniature size and ease of use.
Unfortunately it comes with bloated software which doesn’t quite meet the
privacy trust threshold for me. Since I like the scanner but don’t like the
first-party software, I figured this was a perfect opportunity to see if I
could get <code class="language-plaintext highlighter-rouge">scanimage</code> to work.</p>

<p>Some Googling later, I found the <a href="http://www.sane-project.org">SANE
project</a>, where “SANE” stands for “Scanner
Access Now Easy.” As a macOS user, I immediately searched Homebrew, where I found
<a href="https://formulae.brew.sh/formula/sane-backends#default"><code class="language-plaintext highlighter-rouge">sane-backends</code></a>,
a package which would ostensibly give me the <code class="language-plaintext highlighter-rouge">scanimage</code> binary and the
backend firmware needed for my scanner.</p>

<p>With my scanner plugged in, I ran <code class="language-plaintext highlighter-rouge">scanimage</code> but got the error <code class="language-plaintext highlighter-rouge">scanimage:
no SANE devices found</code>. Huh? My scanner was definitely plugged in. System
Report on the Mac told me so. The <code class="language-plaintext highlighter-rouge">sane-backends</code> package comes with a
helpful binary called <code class="language-plaintext highlighter-rouge">sane-find-scanner</code>, but I got another error there
too: <code class="language-plaintext highlighter-rouge">could not fetch string descriptor: Pipe error</code>. Not very helpful.</p>

<p>Back to Google, to figure out why the program couldn’t find my scanner. A
number of forums referenced these mysterious <code class="language-plaintext highlighter-rouge">.nal</code> files. I decided to see
if I had this <code class="language-plaintext highlighter-rouge">.nal</code> file in the sane-backends installation. Unfortunately
I did not. There’s a file called <code class="language-plaintext highlighter-rouge">etc/sane.d/epjitsu.conf</code> which referenced
a <code class="language-plaintext highlighter-rouge">.nal</code> file for the Fujifilm S1100 I had plugged in, but the file it
referenced didn’t exist. That’s odd – I would have expected “sane-backends”
to include the backends for the scanners.</p>

<p>A bit more Googling and I found a GitHub repository which contained the
necessary files and I cloned it into the directory where the <code class="language-plaintext highlighter-rouge">epjitsu.conf</code>
file was expecting it to be. I re-ran <code class="language-plaintext highlighter-rouge">sane-find-scanner</code> and – hooray! –
it registered. I ran <code class="language-plaintext highlighter-rouge">scanimage</code> and finally got something!</p>

<p>With <code class="language-plaintext highlighter-rouge">scanimage</code> successfully working, I decided to modify Julia’s
<code class="language-plaintext highlighter-rouge">scan.sh</code> script for my needs. I decided to use <code class="language-plaintext highlighter-rouge">tiff</code> as the output format
and use <code class="language-plaintext highlighter-rouge">--batch-prompt</code> so that I could feed the ScanSnap (the default
batch behavior expects you have a feeder scanner which pulls in each page
one at a time without human intervention). I ran my custom script and –
oops! - no <code class="language-plaintext highlighter-rouge">convert</code> binary in sight. This binary is provided by the
<code class="language-plaintext highlighter-rouge">imagemagick</code> package, so back to Homebrew to install that. Once that was
sorted, I successfully made my tiff scans and converted them into a PDF.</p>

<p>I chose TIFF for its quality. When scanning a document, I want it to be
crisp to promote readability and better quality if it gets printed from
these scans. Unfortunately, TIFF files can be massive. A single page was
just over 1MB, and somehow <code class="language-plaintext highlighter-rouge">convert</code> created a PDF file which was <strong>348
MB</strong> large – huge when the input was only 40 1MB files. Luckily Preview on
the Mac has a way to fix this. Open the file, then choose “File -&gt;
Export…”. Under the “Quartz Filter” drop-down, there’s an option to
choose “Black &amp; White” or “Grey Tone”. Choose either and export. (I found
that choosing “Reduce File Size” irreparably reduced the quality, YMMV.)</p>

<p>And finally, the saga was complete. I had my 40-page PDF at a reasonable
size! I hope this is useful to others who don’t want to install gigabytes
of software from their scanner’s vendor but can’t find a way past the
default <code class="language-plaintext highlighter-rouge">sane-backends</code> install. Clearly you need to BYOD: bring your own
driver. Once you have that in place, you’re golden!</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Use Self-Hosted GitHub Actions Runners]]></title>
    <link href="https://byparker.com/blog/2022/self-hosted-github-actions-runners/"/>
    <updated>2022-02-03T00:00:00+00:00</updated>
    <id>https://byparker.com/blog/2022/self-hosted-github-actions-runners</id>
    <content type="html"><![CDATA[<p>GitHub released <a href="https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners">self-hosted Actions runners</a> some time ago. The gist is this: instead of using GitHub-provided machines to run your Actions workflows, you use a computer you control instead. This can be hugely powerful for enterprises and at-home nerds alike. I’m going to share two use-cases that I came up with today.</p>

<p><a href="https://docs.github.com/en/actions/hosting-your-own-runners/adding-self-hosted-runners">Downloading and setting up the runner is fast and easy</a>. You can
add them to a single repo or to whole organizations. Unfortunately, there’s not currently a means of setting a self-hosted runner for a user account –
users can only add them to individual repos.</p>

<p>DISCLAIMER: <a href="https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners#self-hosted-runner-security">Security on self-hosted runners is up to you</a>.
Every workflow you execute on your machine has all the access your user does unless you isolate the runner properly. Proceed at your own risk!</p>

<h3 id="use-case-1-full-text-instapaper-archive">Use case 1: Full-text Instapaper archive</h3>

<p>Instapaper has been around over 10 years now, and I have been a user for almost all of that time. I have thousands of saved articles with highlights and notes that I don’t want to lose. I developed a Go program to pull down my saved articles and generate a Jekyll site: each article is a post with its content from the Instapaper Full-text API endpoint, and each article has 2 data files, one for metadata like “did I star this?” and one with a list of my highlights. A script builds the Jekyll site, then syncs it to a home server where it is served with nginx. Since the home server is running <a href="https://www.tailscale.com">Tailscale</a>, I can easily go to <code class="language-plaintext highlighter-rouge">https://hostname/instapaper-archive</code> to view every article I have ever saved.</p>

<p>All this works well, but it requires that I remember to run this script on my laptop when I have read some articles on Instapaper, and it is closely tied to state on my laptop. Logs are not persisted anywhere. It has all the problems you’d expect with a personal program you’re running by hand. So, how can I automate this? A self-hosted GitHub Actions runner, of course!</p>

<p>GitHub Actions allows you to run workflows on a schedule. Using Cron syntax, you can schedule a workflow to run as often as every 5 minutes. The beauty of a <em>self-hosted</em> runner is that I control the environment: I control the network, I control the disk, and I control the workflow. (There’s a way to fix the network bit by using <a href="https://github.com/tailscale/github-action">tailscale/github-action to setup an ephemeral node</a>, but why eat into my Actions minutes when I can use an idle computer in my home instead?)</p>

<p>I setup the workflow to run the archiver, run jekyll, then rsync the resulting site to my home server for later browsing. Since the archiver works incrementally, I setup a directory outside the working directory that I could write to. This allows me to more easily maintain state between runs.</p>

<p>The key pieces of this workflow are:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">on</span><span class="pi">:</span>
  <span class="na">schedule</span><span class="pi">:</span>
  <span class="c1"># Run every day at 5:30 and 17:30 UTC.</span>
  <span class="pi">-</span> <span class="na">cron</span><span class="pi">:</span> <span class="s1">'</span><span class="s">30</span><span class="nv"> </span><span class="s">5,17</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*</span><span class="nv"> </span><span class="s">*'</span>
</code></pre></div></div>

<p>and</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">runs-on</span><span class="pi">:</span> <span class="s">self-hosted</span>
</code></pre></div></div>

<p>If you have a number of self-hosted Actions runners on your repo, you can tag them with whatever you like and target just a subset:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Finds runners tagged with both "self-hosted" and "mytag"</span>
<span class="na">runs-on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">self-hosted</span><span class="pi">,</span> <span class="nv">mytag</span><span class="pi">]</span>
</code></pre></div></div>

<p>It’s nice to have this kind of scheduled work automated for me running on a computer I trust. But is there anything more critical you can use self-hosted runners for?</p>

<h3 id="use-case-2-scheduled-puppet-runs">Use case 2: Scheduled Puppet runs</h3>

<p>As a self-professed nerd working in the server space for a few years, I maintain a Puppet repo to configure my personal servers.
It’s worked well for me for over 5 years now. I have a cron job on each host that runs <code class="language-plaintext highlighter-rouge">puppet apply</code>. This seems to work, but there’s no transparency and it wastes CPU by running every 5 minutes. If I ran a self-hosted runner on each host instead, I could run Puppet on push to my main branch, instead of waiting for the cron job to run, and I could see the puppet run’s logs much more easily.</p>

<p>I gave this a try on my home server first (since it’s easier to recover a host you have physical access to), and it worked like a charm. I configured the runner as <code class="language-plaintext highlighter-rouge">root</code> (eek!) and installed a systemd service with <code class="language-plaintext highlighter-rouge">./svc.sh install &amp;&amp; ./svc.sh start</code>. I wrote a workflow to pull the latest changes and run <code class="language-plaintext highlighter-rouge">puppet apply</code> in my checkout of the repo on each host, and it worked like a charm.</p>

<p>Since I have multiple hosts, I decided I needed to tag them with their hostname so I could target a job to each host. Each host’s <code class="language-plaintext highlighter-rouge">puppet apply</code> is thus independent so if there’s a problem on one host, it doesn’t affect the functioning of puppet on the others.</p>

<p>In order to run on push, I added:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">main</span><span class="pi">]</span>
</code></pre></div></div>

<p>and I created a job for that host:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span>
  <span class="na">homeserver</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">self-hosted</span><span class="pi">,</span> <span class="nv">homeserver</span><span class="pi">]</span>
    <span class="s">…</span>
    <span class="na">steps</span><span class="pi">:</span> <span class="s">…</span>
</code></pre></div></div>

<p>I pushed this up and it all worked! Changes to the Puppet repo were applied directly to the host by the self-hosted runner. Next, I added a self-hosted runner to my cloud VM and specified a second job in the YAML:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span>
  <span class="na">homeserver</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">self-hosted</span><span class="pi">,</span> <span class="nv">homeserver</span><span class="pi">]</span>
    <span class="s">…</span>
    <span class="na">steps</span><span class="pi">:</span> <span class="s">…</span>
  <span class="na">cloudvm</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">self-hosted</span><span class="pi">,</span> <span class="nv">cloudvm</span><span class="pi">]</span>
    <span class="s">…</span>
    <span class="na">steps</span><span class="pi">:</span> <span class="s">…</span>
</code></pre></div></div>

<p>This worked like a charm. Now I can stop wasting CPU with my cron job and I can easily see the result of any change I make in the GitHub UI.</p>

<h3 id="conclusion">Conclusion</h3>

<p>Self-hosted runners are pretty magical. You can test your code with the computer under your desk! You can apply system configuration changes with a simple Git push! You can run all sorts of code whenever you want! So far I’m really impressed with how these runners “just work” out of the box. What are you using self-hosted runners for?</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[brew gem install jekyll]]></title>
    <link href="https://byparker.com/blog/2021/brew-gem-install-jekyll/"/>
    <updated>2021-12-26T00:00:00+00:00</updated>
    <id>https://byparker.com/blog/2021/brew-gem-install-jekyll</id>
    <content type="html"><![CDATA[<p>TL;DR: An extension to Homebrew allows you to install gems globally on your machine, thus <code class="language-plaintext highlighter-rouge">brew gem install jekyll</code>. Install <code class="language-plaintext highlighter-rouge">brew install brew-gem</code> to use.</p>

<p>I discovered a project called <a href="https://formulae.brew.sh/formula/brew-gem#default"><code class="language-plaintext highlighter-rouge">brew-gem</code></a> which simplifies the gem install process for gems which are system-wide. It installs the gem and its dependencies in an isolated directory and creates an executable stub in <code class="language-plaintext highlighter-rouge">/usr/local/bin</code>. This means you can <code class="language-plaintext highlighter-rouge">brew gem install jekyll</code> and have Jekyll and all its dependencies installed without rvm/rbenv or any other manager! No <code class="language-plaintext highlighter-rouge">sudo</code> needed, either.</p>

<p>Apple bundles Ruby on its Macs, but Homebrew also supplies Ruby so you can pass the flag <code class="language-plaintext highlighter-rouge">--homebrew-ruby</code> to use Homebrew’s Ruby (i.e. from <code class="language-plaintext highlighter-rouge">brew install ruby</code>) or pass <code class="language-plaintext highlighter-rouge">--system-ruby</code> to use the system Ruby version. By default, <code class="language-plaintext highlighter-rouge">brew gem</code> uses the Homebrew Ruby version if it’s installed.</p>

<p>Next time you need a utility written as a Ruby gem, try <code class="language-plaintext highlighter-rouge">brew gem install</code>!</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Work In the Cloud]]></title>
    <link href="https://byparker.com/blog/2021/work-in-the-cloud/"/>
    <updated>2021-12-18T00:00:00+00:00</updated>
    <id>https://byparker.com/blog/2021/work-in-the-cloud</id>
    <content type="html"><![CDATA[<p>As a programmer, I have long cared deeply about having the latest, most
powerful machine to do my work. If I am not left waiting for my tools to do
their job, then I can more quickly do mine.</p>

<p>Recently, I have felt like I never have a powerful-enough machine. Running
Docker for Mac brings even a $2000 or $3000 MacBook Pro (Intel) to its knees.
Issues persist with Apple’s silicon as well, given that it’s relatively new
so the software hasn’t fully caught up yet.</p>

<p>My most recent team at GitHub worked on an internal app. The app was
made up of a series of different services. For development, we used Docker
Compose to run a dev database, a Rails app, a webpack compiler, a background
worker, and probably one or two things I have now forgotten. Running 4 or 5
services in development in Docker for Mac which were all polling for
filesystem events from the host into a VM and into cgroups was a mess.
After I upgraded to a machine with 64GB of RAM, this workflow became much better,
but it was still a huge battery drain and would make the CPU red hot.</p>

<p>We were early experimenters of GitHub Codespaces. Codespaces natively
supports Docker Compose, so setting things up was relatively easy.
Once Codespaces became generally available, I migrated to use it. Every
day, I woke my computer from sleep and reconnected to my Codespace in the
browser. As I worked, I felt more productive than ever. Changes were instantly
reflected. It felt like I had this supercomputer that I could access from anywhere.</p>

<p>A few months ago I began working at Google. Google is famous for its <a href="https://www.youtube.com/watch?v=W71BTkUbdqE">one
monolithic codebase of billions of lines of code</a>. 
No single machine could
accomodate this amount of code, so they invented CitC, a cloud-based file
system overlay of the monolithic codebase. Many Googlers have physical
workstations at their office desks, but pandemic hires are instead given a
virtual machine in the cloud. These machines are given the workstation
treatment. I’m able to remotely access mine through a browser window as though
it were a virtual monitor for the machine. The immense
power of these VMs gives developers the ability to develop massive
applications with relative ease. My experience at Google has only enhanced
my delight for coding in the cloud.</p>

<p>Coding in the cloud can be cheaper too. Assuming approximately 246 working days per year
(261 work days minus 15 days for vacation), a $3,000 machine would cost you
roughly $4.06 per day of work over 3 years. Some companies refresh
machines every 2 years, which raises that cost to $6.10/day. Codespaces
currently costs $0.72/hour for 8 cores, and it automatically shuts off and stops
billing for vCPU after 30 minutes. A normal 8 hour work day includes
meetings and other non-coding work, so let’s assume you’re actively coding
for about 5 hours per day, costing the business $3.60/day or roughly
$2656.89 for the same 3 year working period. This is cheaper than your
physical machine! They can give you a lightweight, inexpensive computer and you
can do absolutely everything in the cloud. If you leave after 1 or 2 years,
the company won’t need to spend the money for your Codespace, thus spending
less on you than if they had given you a big, beefy machine during
onboarding.</p>

<p>I’m sure there are many specific edge-cases I didn’t account for which
might sway the financials one way or the other, but this remains true:
<strong>working in the cloud allows scalability and flexibility in today’s
fast-paced world, and offers true developer delight.</strong></p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[The power of bashrc.d]]></title>
    <link href="https://byparker.com/blog/2021/the-power-of-bashrc-d/"/>
    <updated>2021-05-29T19:38:00+00:00</updated>
    <id>https://byparker.com/blog/2021/the-power-of-bashrc-d</id>
    <content type="html"><![CDATA[<p>Booting a new shell had gotten <em>slow</em>. Opening a new Terminal window was getting tedious. I decided it was time to finally solve this.</p>

<p>I found <a href="https://work.lisk.in/2020/11/20/even-faster-bash-startup.html">a wonderful blog post entitled “Even faster bash startup”</a>, in which the author tackled this same problem.</p>

<p>They started with the <a href="https://github.com/sharkdp/hyperfine#readme"><code class="language-plaintext highlighter-rouge">hyperfine</code></a> utility to benchmark the login:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>hyperfine <span class="nt">-i</span> <span class="s1">'bash -l'</span>
Benchmark <span class="c">#1: bash -l</span>
  Time <span class="o">(</span>mean ± σ<span class="o">)</span>:      1.242 s ±  0.089 s    <span class="o">[</span>User: 648.1 ms, System: 559.0 ms]
  Range <span class="o">(</span>min … max<span class="o">)</span>:    1.129 s …  1.415 s    10 runs
</code></pre></div></div>

<p>Almost 1.5 seconds for bash to login on my machine. But how can you determine what exactly is taking up this time?</p>

<p>The real “Eureka!” moment from this post is under the title “death by a thousand cuts”:</p>

<blockquote>
  <p>My <code class="language-plaintext highlighter-rouge">.bashrc</code> is split into several smaller parts in <code class="language-plaintext highlighter-rouge">~/.bashrc.d</code>, so I can profile these and see if anything stands out.</p>
</blockquote>

<p>Genius. I followed suit and created a <code class="language-plaintext highlighter-rouge">~/.bashrc.d</code> directory (saved in my dotfiles repo of course) and split up the contents of my <code class="language-plaintext highlighter-rouge">.bashrc</code> file into discrete files. My <code class="language-plaintext highlighter-rouge">~/.bashrc</code> file <a href="https://github.com/parkr/dotfiles/blob/a9c603e2e5d300c5b3d4fdb50b6874db6e85a2c6/osx/bashrc.symlink#L4-L40">became largely setting up the benchmarking and sourcing other files</a>. I could then run <code class="language-plaintext highlighter-rouge">__bashrc_bench=1 . ~/.bashrc</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ __bashrc_bench</span><span class="o">=</span>1 <span class="nb">.</span> ~/.bashrc
/Users/parker/.bashrc.d/10_env.sh: 0.001
/Users/parker/.bashrc.d/20_history.sh: 0.000
/Users/parker/.bashrc.d/30_prompt.sh: 0.000
/Users/parker/.bashrc.d/40_completion.sh /usr/local/opt/fzf/shell/key-bindings.bash: 0.001
/Users/parker/.bashrc.d/40_completion.sh: 0.001
/Users/parker/.bashrc.d/41_brew_completion.sh /usr/local/etc/bash_completion.d/brew: 0.005
/Users/parker/.bashrc.d/41_brew_completion.sh /usr/local/etc/bash_completion.d/cheat.bash: 0.000
/Users/parker/.bashrc.d/41_brew_completion.sh /usr/local/etc/bash_completion.d/fd.bash: 0.001
/Users/parker/.bashrc.d/41_brew_completion.sh /usr/local/etc/bash_completion.d/gh: 0.015
/Users/parker/.bashrc.d/41_brew_completion.sh /usr/local/etc/bash_completion.d/git-completion.bash: 0.010
/Users/parker/.bashrc.d/41_brew_completion.sh /usr/local/etc/bash_completion.d/git-prompt.sh: 0.002
/Users/parker/.bashrc.d/41_brew_completion.sh /usr/local/etc/bash_completion.d/rg.bash: 0.001
/Users/parker/.bashrc.d/41_brew_completion.sh /usr/local/etc/bash_completion.d/youtube-dl.bash-completion: 0.000
/Users/parker/.bashrc.d/41_brew_completion.sh /usr/local/share/autojump/autojump.bash: 0.013
/Users/parker/.bashrc.d/41_brew_completion.sh: 0.050
/Users/parker/.bashrc.d/50_aliases.sh: 0.004
/Users/parker/.bashrc.d/60_functions.sh: 0.000
/Users/parker/.bashrc.d/70_git_dotfiles.sh: 0.000
/Users/parker/.bashrc.d/80_fzf.sh: 0.000
/Users/parker/.bashrc.d/81_rbenv.sh /usr/local/opt/rbenv/completions/rbenv.bash: 0.000
/Users/parker/.bashrc.d/81_rbenv.sh: 1.068
/Users/parker/.bashrc.d/82_iterm2.sh: 0.000
/Users/parker/.bashrc.d/99_localrc.sh: 0.000
</code></pre></div></div>

<p>This is just from today. 15ms on the <code class="language-plaintext highlighter-rouge">gh</code> auto-completion, 13ms on the autojump completion, and… 1068ms on rbenv? It’s not from the rbenv bash completion, so it must be something in the script itself. This is almost 85% of the total bash boot time that we found earlier with hyperfine!</p>

<p>I recently found myself discussing the nuances of building openssl for rbenv ruby installations on macOS, and one of the maintainers of the <code class="language-plaintext highlighter-rouge">ruby-build</code> installation utility had recommended using the Homebrew-managed openssl, so I added the following line to my <code class="language-plaintext highlighter-rouge">81_rbenv.sh</code> file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">RUBY_CONFIGURE_OPTS</span><span class="o">=</span><span class="s2">"--with-openssl-dir=</span><span class="si">$(</span>brew <span class="nt">--prefix</span> openssl<span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p>Hm, exporting a variable is extremely fast. How long does <code class="language-plaintext highlighter-rouge">brew --prefix openssl</code> take?</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>hyperfine <span class="s1">'brew --prefix openssl'</span>
Benchmark <span class="c">#1: brew --prefix openssl</span>
  Time <span class="o">(</span>mean ± σ<span class="o">)</span>:      1.063 s ±  0.019 s    <span class="o">[</span>User: 561.5 ms, System: 478.6 ms]
  Range <span class="o">(</span>min … max<span class="o">)</span>:    1.032 s …  1.094 s    10 runs
</code></pre></div></div>

<p>Yikes. That entire script’s time is just running <code class="language-plaintext highlighter-rouge">brew</code>! I wonder: could we hardcode this instead of calling <code class="language-plaintext highlighter-rouge">brew</code>?</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>brew <span class="nt">--prefix</span> openssl
/usr/local/opt/openssl@1.1
<span class="nv">$ </span><span class="nb">ls</span> <span class="nt">-l</span> /usr/local/opt/openssl<span class="k">*</span>
lrwxr-xr-x  1 parker  admin  28 Apr  1 22:03 /usr/local/opt/openssl -&gt; ../Cellar/openssl@1.1/1.1.1k
lrwxr-xr-x  1 parker  admin  28 Apr  1 22:03 /usr/local/opt/openssl@1.1 -&gt; ../Cellar/openssl@1.1/1.1.1k
</code></pre></div></div>

<p>Yes! It looks like I could use either of those two paths instead. In order to be modular, it’s best to use a variable. How about <code class="language-plaintext highlighter-rouge">$HOMEBREW_PREFIX</code>? Thus, the line becomes:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">HOMEBREW_PREFIX</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">HOMEBREW_PREFIX</span>:<span class="p">=</span><span class="s2">"/usr/local"</span><span class="k">}</span><span class="s2">"</span>
<span class="nb">export </span><span class="nv">RUBY_CONFIGURE_OPTS</span><span class="o">=</span><span class="s2">"--with-openssl-dir=</span><span class="k">${</span><span class="nv">HOMEBREW_PREFIX</span><span class="k">}</span><span class="s2">/opt/openssl"</span>
</code></pre></div></div>

<p>Does that fix our issue?</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>hyperfine <span class="nt">-i</span> <span class="s1">'bash -l'</span>
Benchmark <span class="c">#1: bash -l</span>
  Time <span class="o">(</span>mean ± σ<span class="o">)</span>:      49.4 ms ±  10.2 ms    <span class="o">[</span>User: 32.0 ms, System: 14.4 ms]
  Range <span class="o">(</span>min … max<span class="o">)</span>:    42.0 ms … 117.3 ms    62 runs
</code></pre></div></div>

<p>50ms? Yes, that’s significantly better than 1200ms! This is a huge success, with many thanks to <code class="language-plaintext highlighter-rouge">~/.bashrc.d</code> for making the benchmarking of each snippet so much easier.</p>

<p>Do you have a complicated <code class="language-plaintext highlighter-rouge">~/.bashrc</code> or <code class="language-plaintext highlighter-rouge">~/.zshrc</code>? Consider following this same method! It makes tracking down those pesky slow start-up scripts so much easier. Reusing this benchmarking code in bash is easy too – the combination of <code class="language-plaintext highlighter-rouge">$TIMEFORMAT</code> and <code class="language-plaintext highlighter-rouge">time</code> makes this a simple proposition. Profiling bash scripts of all shapes and sizes has never been so easy!</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Don't Use the System Ruby; Use a ruby version manager instead]]></title>
    <link href="https://byparker.com/blog/2021/don-t-use-the-system-ruby-use-a-ruby-version-manager-instead/"/>
    <updated>2021-05-20T16:09:00+00:00</updated>
    <id>https://byparker.com/blog/2021/don-t-use-the-system-ruby-use-a-ruby-version-manager-instead</id>
    <content type="html"><![CDATA[<p>Ruby can be notoriously challenging to get setup initially. You might run
into permissions issues when running <code class="language-plaintext highlighter-rouge">gem install</code> or you might go to
install gems and it tells you that you’re using the wrong version of ruby!
These installation woes are the biggest blocker for most folks wanting to
use Ruby packages like <code class="language-plaintext highlighter-rouge">jekyll</code> where Ruby is an implementation detail and
unrelated to what they’re actually trying to do.</p>

<p>How do Rubyists fix this? <strong>They use a Ruby version manager.</strong></p>

<p>There are many Ruby version managers. My preference is <code class="language-plaintext highlighter-rouge">rbenv</code>, but <code class="language-plaintext highlighter-rouge">rvm</code>,
<code class="language-plaintext highlighter-rouge">chruby</code>, and <code class="language-plaintext highlighter-rouge">asdf</code> are all good options.</p>

<p>Getting setup with <code class="language-plaintext highlighter-rouge">rbenv</code> is easy on a Mac (<a href="https://github.com/rbenv/rbenv#installation">full installation instructions</a>):</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">#</span><span class="w"> </span>1. Install rbenv &amp; ruby-build
<span class="gp">$</span><span class="w"> </span>brew <span class="nb">install </span>rbenv ruby-build
<span class="gp">#</span><span class="w"> </span>2. Load rbenv into your shell environment persistently <span class="o">(</span>.bashrc <span class="k">for </span>bash, .zshrc <span class="k">for </span>zsh<span class="o">)</span>
<span class="gp">$</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'eval $(rbenv init -)'</span> <span class="o">&gt;&gt;</span> ~/.bashrc <span class="o">&amp;&amp;</span> <span class="nb">.</span> ~/.bashrc <span class="c"># for bash</span>
<span class="gp">$</span><span class="w"> </span><span class="nb">echo</span> <span class="s1">'eval $(rbenv init -)'</span> <span class="o">&gt;&gt;</span> ~/.zshrc <span class="o">&amp;&amp;</span> <span class="nb">.</span> ~/.zshrc <span class="c"># for zsh</span>
<span class="gp">#</span><span class="w"> </span>3. List versions available to <span class="nb">install</span><span class="o">!</span> Choose one.
<span class="gp">$</span><span class="w"> </span>rbenv <span class="nb">install</span> <span class="nt">--list</span>
<span class="gp">#</span><span class="w"> </span>4. Install that version!
<span class="gp">$</span><span class="w"> </span>rbenv <span class="nb">install </span>2.7.3
<span class="gp">#</span><span class="w"> </span>5. Set that version as the new default on your machine.
<span class="gp">$</span><span class="w"> </span>rbenv global 2.7.3
<span class="gp">#</span><span class="w"> </span>Override <span class="k">for </span>a specific directory by running <span class="sb">`</span>rbenv <span class="nb">local</span> &lt;new_version&gt;<span class="sb">`</span>
</code></pre></div></div>

<p>This installs this version of ruby into <code class="language-plaintext highlighter-rouge">$HOME/.rbenv/versions</code>. Since this
is in your home directory, there are no permissions issues. Gems are
installed inside each individual version (see <code class="language-plaintext highlighter-rouge">gem env GEM_PATH</code> for the
exact paths) so it’s much easier to install.</p>

<p>Running <code class="language-plaintext highlighter-rouge">rbenv global &lt;version&gt;</code> means that your shell will refer to that
version by default. Running <code class="language-plaintext highlighter-rouge">gem install</code> in any directory will install
gems using that version, and running <code class="language-plaintext highlighter-rouge">ruby &lt;file&gt;</code> or a gem executable will
use that Ruby version.</p>

<p>Different projects may have different Ruby versions. Rbenv makes specifying
which Ruby version you want easy: inside your project’s root directory, run
<code class="language-plaintext highlighter-rouge">rbenv local &lt;version&gt;</code> (it will prompt you to install the version if it’s
not present). This is really helpful when you need to be switching between
versions but don’t want to remember. It will write out a <code class="language-plaintext highlighter-rouge">.ruby-version</code>
file so be sure to commit that to your source control!</p>

<p>Want to delete a version you’re not using anymore? Easy: <code class="language-plaintext highlighter-rouge">rm -r ~/.rbenv/versions/&lt;version&gt;</code>.</p>

<p>Using <code class="language-plaintext highlighter-rouge">rbenv</code> makes your life easier! I have been happily using rbenv for
many years now and can easily install and use gems and manage ruby versions
with ease.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Sign commits with a GPG key using a passphrase with pinentry-mac]]></title>
    <link href="https://byparker.com/blog/2021/gpg-pinentry-mac-git/"/>
    <updated>2021-02-23T00:00:00+00:00</updated>
    <id>https://byparker.com/blog/2021/gpg-pinentry-mac-git</id>
    <content type="html"><![CDATA[<p>With the increasing prevalence of inserting malicious code into
commonly-used open source projects on the rise, it might be time to enforce
commit signing for your open source project. This prevents someone from
spoofing your identity on commits they make. If it’s not signed with your
key, then it’s not verified and the UI will show this.</p>

<p>When setting up gpg (v2) to automatically sign your commit messages, you
may run into an error:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>error: gpg failed to sign the data
fatal: failed to write commit object
</code></pre></div></div>

<p>If you re-run your <code class="language-plaintext highlighter-rouge">git commit</code> command with <code class="language-plaintext highlighter-rouge">GIT_TRACE=1</code>, you can see it
running <code class="language-plaintext highlighter-rouge">gpg --status-fd=2 -bsau &lt;your-signing-key&gt;</code> but failing.</p>

<p>The cause of this is the <code class="language-plaintext highlighter-rouge">pinentry</code> process. By default <code class="language-plaintext highlighter-rouge">gpg</code> uses the
<code class="language-plaintext highlighter-rouge">pinentry</code> program which requires a TTY and access to the shell’s stdin.
This doesn’t appear to function properly on a Mac. Instead, you should use
<a href="https://github.com/GPGTools/pinentry/tree/master/macosx"><code class="language-plaintext highlighter-rouge">pinentry-mac</code></a>.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>brew <span class="nb">install </span>pinentry-mac
<span class="gp">$</span><span class="w"> </span><span class="nb">echo</span> <span class="s2">"pinentry-program /usr/local/bin/pinentry-mac"</span> <span class="o">&gt;&gt;</span> ~/.gnupg/gpg-agent.conf
<span class="gp">$</span><span class="w"> </span>gpgconf <span class="nt">--kill</span> gpg-agent
</code></pre></div></div>

<p>Re-run your <code class="language-plaintext highlighter-rouge">git commit</code> command and you should see a GUI pop-up to request
your GPG key’s passphrase. Enter it and the commit will succeed!</p>

<p><img src="/img/gpg-pinentry-mac.png" alt="Screenshot of pinentry-mac where you can enter your password." /></p>

<p>You can setup automatic signing with the following commands:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>git config <span class="nt">--global</span> user.signingkey &lt;key_id&gt;
<span class="gp">$</span><span class="w"> </span>git config <span class="nt">--global</span> commit.sign <span class="nb">true</span>
<span class="gp">$</span><span class="w"> </span>git config <span class="nt">--global</span> commit.gpgSign <span class="nb">true</span>
</code></pre></div></div>

<p>Make sure to upload your GPG key to your GitHub account:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>gpg <span class="nt">--armor</span> <span class="nt">--export</span> &lt;key_id&gt;
</code></pre></div></div>

<p>Happy signing!</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Fitness+]]></title>
    <link href="https://byparker.com/blog/2021/fitness-plus/"/>
    <updated>2021-01-18T00:00:00+00:00</updated>
    <id>https://byparker.com/blog/2021/fitness-plus</id>
    <content type="html"><![CDATA[<p>Fitness+ is a fantastic addition to your fitness regimen. From great trainers to broad variety of workouts, it will bring you happiness in the journey to achieving your fitness goals.</p>

<p>My first workout with Fitness+ was over the holiday break, shortly after it was first released. We couldn’t attend class at our normal gym and needed a good sweat. We’ve been huge fans of <a href="https://fitnessblender.com">Fitness Blender</a> for years, using their free YouTube videos to get a good sweat in while away from home. Since 3 months of free Fitness+ came with the purchase of an Apple Watch Series 6, I though we’d give it a go.</p>

<p>Our first workout was a <strong><a href="https://en.wikipedia.org/wiki/High-intensity_interval_training">HIIT</a></strong> class with Bakari. It was a burner! We really liked the movements that Bakari picked out, and enjoyed his joyful approach to pushing through discomfort. Each move had a modification to reduce impact, which made everything all the more accessible. Don’t want to jump today? No worries, here’s a low-impact modification. We tried workouts of all durations, with our favorites being longer for that really satisfying burn. HIIT from Fitness+ checked all our boxes and left us sore and energized.</p>

<p>Like thousands of households across the U.S., we purchased a <strong>rowing</strong> erg this year for our home. I gave a 20-minute workout with Anja a try, rowing away to some “Upbeat Anthems” and the encouraging coaching from Anja. The structure was on point and you can scale your effort up and down depending on what you’re looking for. Two thumbs up here.</p>

<p>Next, I tried <strong>yoga</strong>. As someone who’s only dipped his toe into yoga in the past, this felt fairly challenging to me. Some trainers move through poses too quickly and can leave an inexperienced person frustrated. Others offer a more measured approach. I generally pick shorter sessions in yoga to help ensure it’s a good experience.</p>

<p>Most recently, we tried <strong>dance</strong>! What a thrill. We laughed the whole time, trying desperately to stay in sync with the instructor. These are workouts you’d want to do over and over to get the most out of them unless you’re an experienced dancer. We were lost half the time! Every time the instructor said “4 steps,” we were ready for 4 steps but then he took 4 steps <em>backward</em>. Things like that trip up casual consumers.</p>

<p><strong>Time to Walk</strong> was recently released and immediately stuck itself at the top of my Workouts app on Apple Watch, which is extremely annoying. The list of workout types (e.g. HIIT, Rowing, Outdoor Run, etc) are ranked by how much you use them – except now “Time to Walk” takes top spot even though I’ve never used it. This goes against the muscle memory I have of always tapping the top spot since it’s my most-often-used workout. No longer. I am surprised Apple would make such a stupid move – frankly, I don’t want to see that if I don’t use it.</p>

<p>Fitness+ has been a great addition to our fitness regimen, and especially shines on days we can’t attend the gym. I’d recommend it to anyone in the Apple ecosystem who is looking to sweat a little!</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Change your GitHub repos' default branch names]]></title>
    <link href="https://byparker.com/blog/2020/change-your-github-repos-default-branch-names/"/>
    <updated>2020-06-23T00:00:00+00:00</updated>
    <id>https://byparker.com/blog/2020/change-your-github-repos-default-branch-names</id>
    <content type="html"><![CDATA[<p>Over the weekend, I wrote a very simple tool for GitHub to change the default branch of your repos. The default branch name that Git and GitHub currently gives you is seen by many as insensitive. There’s no technical reason for keeping the name as-is so I wrote a tool to automate the process of switching for you!</p>

<p><a href="/go/github-utils/cmd/github-change-default-branch"><code class="language-plaintext highlighter-rouge">github-change-default-branch</code></a> is a command-line tool which walks through all repos from a user or organization on GitHub and prompts you to ask whether you’d like to change the default branch to a new name. You can change the target name with a flag:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>github-change-default-branch <span class="nt">-login</span><span class="o">=</span>parkr <span class="nt">-new-name</span><span class="o">=</span>main
<span class="go">parkr/auto-reply - :loop: Handle GitHub webhooks and manage issues on your repositories. Currently runs @jekyllbot.
  https://github.com/parkr/auto-reply
</span><span class="gp">  change default branch from "master" to "main"? (y/n) &gt;</span><span class="w">
</span></code></pre></div></div>

<p>If you choose to change the default branch, the program will do the following:</p>

<ol>
  <li>Add a new ref with the given name at the same commit SHA as the current default branch</li>
  <li>Update the default branch setting for the repo to point to the new ref</li>
  <li>Delete the old default branch ref from the repo</li>
</ol>

<p>To install into <code class="language-plaintext highlighter-rouge">$GOPATH/bin</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>go get github.com/parkr/github-utils/cmd/github-change-default-branch
</code></pre></div></div>

<p>Changing the name of the default branch for your repos has never been easier, so now there’s no excuse to keep the current insensitively-named default.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Goodbye, Amazon Prime]]></title>
    <link href="https://byparker.com/blog/2020/goodbye-amazon-prime/"/>
    <updated>2020-06-01T00:00:00+00:00</updated>
    <id>https://byparker.com/blog/2020/goodbye-amazon-prime</id>
    <content type="html"><![CDATA[<p>After a decade as an Amazon Prime member, I have canceled my membership.</p>

<p>Amazon Prime is a collection of services you subscribe to for a monthly or yearly fee. This includes video, music, and free fast delivery. Investors are always looking to Prime membership numbers as a sign of the strength of Amazon’s business.</p>

<p>Amazon has abused its market power and great wealth at great cost to its workers. This has been apparent for years, but truly came into full clarity during the COVID-19 crisis. In its fulfillment centers, workers are not being protected. Workers are being told they will have to forfeit all pay if they get sick (as a result of their work or not)<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>. The people who turn our orders into packages at our door are being squarely screwed in this crisis<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>, while Amazon rakes in profits and market share<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>.</p>

<p>Too many people are hurting.</p>

<p>There are myriad other ways to buy the goods that Amazon sells. I’ll be buying from small businesses directly for all my shopping that would have previously been on Amazon. In light of COVID-19, I can get everything online from many local businesses.</p>

<p>It’s often said that we vote with our dollars. I, for one, will be voting for something new. Join others<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup>,<sup id="fnref:5" role="doc-noteref"><a href="#fn:5" class="footnote" rel="footnote">5</a></sup> who are voting “no” to Amazon’s exploitation.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>https://www.theguardian.com/technology/2020/may/07/amazon-warehouse-workers-coronavirus-time-off-california <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>https://www.cbsnews.com/news/amazon-workers-with-coronavirus-60-minutes-2020-05-10/ <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>https://www.vox.com/recode/2020/4/10/21215953/amazon-fresh-walmart-grocery-delivery-coronavirus-retail-store-closures <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p>https://mashable.com/article/cancel-amazon-prime/ <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:5" role="doc-endnote">
      <p>https://www.washingtonpost.com/technology/2020/05/04/top-amazon-executive-quits-over-firings-warehouse-workers-climate-activists/ <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[AirPods Pro]]></title>
    <link href="https://byparker.com/blog/2019/airpods-pro/"/>
    <updated>2019-11-03T00:00:00+00:00</updated>
    <id>https://byparker.com/blog/2019/airpods-pro</id>
    <content type="html"><![CDATA[<p>The AirPods Pro are incredibly good.</p>

<p>The <strong>fit</strong> out of the box is comfortable. These buds stay in your ears.
Whether racing down the stairwell with your pup or going out for a jog,
you won’t have to think about them falling out. They started getting a bit
looser after a few miles of running each time I smiled at a passerby
and needed to be pushed farther into my ear more frequently.</p>

<p>The <strong>vent</strong> prevents you from feeling too clogged and only hearing your haggard breath during a workout. This was my primary complaint with the Jaybird Vista: every breath, every step, and the rushing wind would all scream through the music during a workout. This vent makes these buds so much more practical for me.</p>

<p><strong>Noise cancellation</strong> is insanely good. When you put the first earbud in,
you won’t notice any cancellation. This detail prevents that “clogged-up”
feeling you might get with an earplug in just one ear. Once you put in the
second earbud, the world fades away. <strong>Transparency</strong> is very good and very easy to activate when someone wants to speak to you.</p>

<p>The <strong>sound quality</strong> is great. I didn’t really mind the AirPods since so much other sound leaked in. With more isolated sound, these buds are impressive in their wide range.</p>

<p><strong>Charging case</strong> is oddly shaped but not too dissimilar to the regular AirPods
case. Having wireless Qi charging by default is a game changer.</p>

<p>The <strong>controls</strong> are pretty tough to use. I have pretty skinny fingers, and
getting a hold of the stem to squeeze it without dislodging the earbud is
still something I still haven’t figured out. In this way I like the original AirPods better. But the squeeze approach
offers short and long press options, which increase the possibilities. This improves the usefulness of the buds.</p>

<p>The <strong>look</strong> is sleek. I have smaller ears and they just fade away.</p>

<p>The <strong>price</strong> is insanely high. We really shouldn’t be spending so much on headphones. Since I use these so often though and my original AirPods are starting to have connection issues, I figured I’d give them a shot. The whole true wireless headphone market is overpriced.</p>

<p><strong>Overall</strong>, I am very happy with the AirPods Pro.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Generating a Golang-compatible SSH RSA key pair]]></title>
    <link href="https://byparker.com/blog/2019/generating-a-golang-compatible-ssh-rsa-key-pair/"/>
    <updated>2019-02-14T04:27:14+00:00</updated>
    <id>https://byparker.com/blog/2019/generating-a-golang-compatible-ssh-rsa-key-pair</id>
    <content type="html"><![CDATA[<p>It took me an embarassingly long time to discover how to do this today, so
I thought I’d write it down and send it into the ether of the Internet
hoping it would help someone else someday.</p>

<p>So I have this great golang server which connects to a remote host over
SSH. Not so bad so far, <code class="language-plaintext highlighter-rouge">golang.org/x/crypto/ssh</code> makes this pretty easy.
The hard part is that the normal tools we use to generate SSH keys are
not compatible with this crypto/ssh library.</p>

<p>My goal was to have a private key, PEM encoded, and encrypted with a
passphrase. This would be loaded by my Go code. Then, I wanted to generate
a public key for that private key that was in that funky <code class="language-plaintext highlighter-rouge">authorized_keys</code>
SSH form of <code class="language-plaintext highlighter-rouge">key-type key-value key-comment</code>.</p>

<p>Using ssh-keygen gets you a PEM-encoded private key and a properly encoded
public key, but when you try to load the private key into the Go library,
it chokes on the passphrase. The trick is that several important headers
are missing which tell the Go library that it’s encrypted.</p>

<h2 id="the-solution">The Solution</h2>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">#</span><span class="w"> </span>To generate the private key, run the following:
<span class="gp">$</span><span class="w"> </span>openssl genrsa <span class="nt">-des3</span> <span class="nt">-out</span> private.pem 4096
</code></pre></div></div>

<p>Now you have your private key. Now you need to generate the public key.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">#</span><span class="w"> </span>To generate the <span class="k">*</span>public<span class="k">*</span> key from your private key, run the following:
<span class="gp">$</span><span class="w"> </span>openssl rsa <span class="nt">-in</span> private.pem <span class="nt">-outform</span> PEM <span class="nt">-pubout</span> <span class="nt">-out</span> public.pem
</code></pre></div></div>

<p>Now you have a PEM format for your public key. Nice! This can’t be used
with SSH’s <code class="language-plaintext highlighter-rouge">authorized_keys</code> file though, so we’ll have to do one more
conversion:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">#</span><span class="w"> </span>To generate the ssh-rsa public key format, run the following:
<span class="gp">$</span><span class="w"> </span>ssh-keygen <span class="nt">-f</span> public.pem <span class="nt">-i</span> <span class="nt">-mPKCS8</span> <span class="o">&gt;</span> id_rsa.pub
</code></pre></div></div>

<p>Now you have a public key in the format OpenSSH expects.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">private.pem</code> - your encrypted, PEM-encoded private key</li>
  <li><code class="language-plaintext highlighter-rouge">id_rsa.pub</code> - your OpenSSH-compatible public key</li>
</ul>

<p>That seems to do the trick for me, and SSH connections are working great
between my Go service and my servers. :tada: :sparkles:</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Use Git & GitHub to manage your DNS with OctoDNS]]></title>
    <link href="https://byparker.com/blog/2018/use-git-github-to-manage-your-dns-with-octodns/"/>
    <updated>2018-12-14T17:12:42+00:00</updated>
    <id>https://byparker.com/blog/2018/use-git-github-to-manage-your-dns-with-octodns</id>
    <content type="html"><![CDATA[<p>DNS is one of those services that still requires logging into some web
portal and clicking buttons and entering data. Heaven forbid you enter a
bad IP address and take down your service – there’s no “rollback to
previous value” option. Want to track your configurations over time? Sorry,
not something your web portal can often do. DNS configuration should be simple,
trackable, and automatable.</p>

<p>Good news! You can take charge of your DNS configuration with Git, GitHub
and OctoDNS.</p>

<p><a href="https://github.com/github/octodns">OctoDNS</a> is a library for managing your DNS, which ships with a series of
commands for syncing, dumping, and reporting. OctoDNS was originally developed
at GitHub to aid in tracking DNS across our domains and in helping integrate
DNS management into our normal tools, like Git, our deployment stack, and so on.</p>

<p>OctoDNS operates kind of like <code class="language-plaintext highlighter-rouge">rsync</code>: you specify a source (usually YAML files)
and specify a destination or destinations (DNS providers) and ask it to sync.
Any configurations present in the source configuration (YAML) will sync to the
destination (DNS providers). I haven’t tried, but theoretically you can even
sync from one DNS provider to another and skip YAML altogether! But the point of
this blog post is to share how we manage DNS with Git &amp; GitHub so we’ll be using
YAML.</p>

<h2 id="getting-started">Getting Started</h2>

<h3 id="1-configuration">1. Configuration</h3>

<p>We’ll create our repository and dump a zone to get started. Create a repository
on GitHub, initialize with a readme, and clone:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">~$</span><span class="w"> </span>git clone https://github.com/YOURUSERNAME/dns
<span class="gp">~$</span><span class="w"> </span><span class="nb">cd </span>dns
</code></pre></div></div>

<p>Now, make a <code class="language-plaintext highlighter-rouge">config</code> directory and specify the source &amp; destination for your
configurations. At GitHub we name this file after its environment. We’ll be
fancy and call our environment “production.”</p>

<p>In this example, I’ll use Cloudflare as my DNS provider but you can use <a href="https://github.com/github/octodns#providers">any other
supported providers</a>. The records they support depend on their API capabilities,
so make sure to check that your provider supports creating the types of records you want.
In this file, we’ll use the built-in environment variable expansion. This means
any <code class="language-plaintext highlighter-rouge">octodns-*</code> commands will need to be run with these variables set.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">~/dns$</span><span class="w"> </span><span class="nb">mkdir </span>config
<span class="gp">~/dns$</span><span class="w"> </span><span class="nb">cat</span> <span class="o">&gt;</span> config/production.yaml <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh">
</span><span class="go">---
providers:
  config:
    class: octodns.provider.yaml.YamlProvider
    directory: ./config
  cloudflare:
    class: octodns.provider.cloudflare.CloudflareProvider
    email: env/CLOUDFLARE_EMAIL
    token: env/CLOUDFLARE_TOKEN

zones:
  example.com.:
    sources:
      - config
    targets:
      - cloudflare
EOF
</span></code></pre></div></div>

<p>Ok, so the configuration specifies two things: the providers, and the zones.</p>

<p>The providers are your sources and destinations for the DNS records. Here, we’re
specifying a provider called <code class="language-plaintext highlighter-rouge">config</code> which reads the YAML from <code class="language-plaintext highlighter-rouge">./config</code>. We
also specify a provider called <code class="language-plaintext highlighter-rouge">cloudflare</code> which uses Cloudflare’s API and
requires the <code class="language-plaintext highlighter-rouge">CLOUDFLARE_EMAIL</code> and <code class="language-plaintext highlighter-rouge">CLOUDFLARE_TOKEN</code> environment variables.</p>

<p>The zones are your domains! You can specify unique sources and targets for each.
“Target” is our destination, and you can specify as many as you wish! They must
be configured in the <code class="language-plaintext highlighter-rouge">providers</code> dictionary to use them.</p>

<h3 id="2-dump-your-zone">2. Dump your zone</h3>

<p>Now that you have your configuration file in place, you can run OctoDNS! First,
we’ll dump the zone you added to your configuration file. OctoDNS will create a
new YAML file with all the records it could find in the target you specify.</p>

<p>We’ll use Docker to make installation a little simpler, but if you’re into
managing your own Python virtualenv and such, feel free to do that. The OctoDNS
repository’s readme has great documentation on running it straight on your host.</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
<span class="go">    -e CLOUDFLARE_EMAIL=yourlogin@gmail.com \
    -e CLOUDFLARE_TOKEN=yourtoken \
</span><span class="gp">    -v $</span><span class="o">(</span><span class="nb">pwd</span><span class="o">)</span>:/opt/dns <span class="se">\</span>
<span class="go">    parkr/octodns:v0.9.4 \
    octodns-dump \
    --config-file /opt/dns/config/production.yaml \
    --output-dir /opt/dns/config \
    "example.com." \
    cloudflare
</span></code></pre></div></div>

<p>This will print out a YAML file in <code class="language-plaintext highlighter-rouge">config/example.com.yaml</code>, containing your
DNS entries (comments my own):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="c1"># These are the top-level DNS records for example.com.</span>
<span class="c1"># This specifies:</span>
<span class="c1">#    1. ALIAS example.com to subdomainarecords.example.com</span>
<span class="c1">#    2. MX example.com to mxa.emailprovider.com at priority 10 and mxb.emailprovider.com at priority 10</span>
<span class="c1">#    3. TXT example.com with "v=spf1..."</span>
<span class="pi">?</span> <span class="s1">'</span><span class="s">'</span>
<span class="pi">:</span> <span class="pi">-</span> <span class="na">ttl</span><span class="pi">:</span> <span class="m">300</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">ALIAS</span>
    <span class="na">value</span><span class="pi">:</span> <span class="s">subdomainarecords.example.com.</span>
  <span class="pi">-</span> <span class="na">ttl</span><span class="pi">:</span> <span class="m">300</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">MX</span>
    <span class="na">values</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">exchange</span><span class="pi">:</span> <span class="s">mxa.emailprovider.com.</span>
      <span class="na">preference</span><span class="pi">:</span> <span class="m">10</span>
    <span class="pi">-</span> <span class="na">exchange</span><span class="pi">:</span> <span class="s">mxb.emailprovider.com.</span>
      <span class="na">preference</span><span class="pi">:</span> <span class="m">10</span>
  <span class="pi">-</span> <span class="na">ttl</span><span class="pi">:</span> <span class="m">300</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">TXT</span>
    <span class="na">value</span><span class="pi">:</span> <span class="s">v=spf1 include:emailprovider.com ~all</span>
<span class="c1"># Here is an example of a CNAME record, subdomaincname.example.com.</span>
<span class="c1"># It points back to example.com.</span>
<span class="na">subdomaincname</span><span class="pi">:</span>
  <span class="na">ttl</span><span class="pi">:</span> <span class="m">300</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">CNAME</span>
  <span class="na">value</span><span class="pi">:</span> <span class="s">example.com.</span>
<span class="c1"># And here is an example of an A record with corresponding IPv6 AAAA record,</span>
<span class="c1"># subdomainarecords.example.com.</span>
<span class="na">subdomainarecords</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">ttl</span><span class="pi">:</span> <span class="m">300</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">A</span>
  <span class="na">value</span><span class="pi">:</span> <span class="s">100.100.100.100</span>
<span class="pi">-</span> <span class="na">ttl</span><span class="pi">:</span> <span class="m">300</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">AAAA</span>
  <span class="na">value</span><span class="pi">:</span> <span class="s">2001:2001:2001:2001:2001:2001:2001:2001</span>
</code></pre></div></div>

<p>Whew! This is great! Now we can commit this file and propose a change.</p>

<h3 id="3-syncing-your-zones">3. Syncing your zones</h3>

<p>Let’s make a change. Let’s modify the A records for subdomainarecords.example.com.
To do this, we’ll update the YAML:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">subdomainarecords</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">ttl</span><span class="pi">:</span> <span class="m">300</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">A</span>
  <span class="na">value</span><span class="pi">:</span> <span class="s">300.300.300.300</span>
<span class="pi">-</span> <span class="na">ttl</span><span class="pi">:</span> <span class="m">300</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">AAAA</span>
  <span class="na">value</span><span class="pi">:</span> <span class="s">4001:4001:4001:4001:4001:4001:4001:4001</span>
</code></pre></div></div>

<p>Now we’ll test our changes with <code class="language-plaintext highlighter-rouge">octodns-sync</code>:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
<span class="go">    -e CLOUDFLARE_EMAIL=yourlogin@gmail.com \
    -e CLOUDFLARE_TOKEN=yourtoken \
</span><span class="gp">    -v $</span><span class="o">(</span><span class="nb">pwd</span><span class="o">)</span>:/opt/dns <span class="se">\</span>
<span class="go">    parkr/octodns:v0.9.4 \
    octodns-sync --config-file /opt/dns/config/production.yaml
</span></code></pre></div></div>

<p>It will print out some beautiful output showing the before and after picture
(it will NOT apply the changes yet):</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>********************************************************************************
* example.com.
********************************************************************************
* cloudflare (CloudflareProvider)
*   Update
*     &lt;AaaaRecord AAAA 300, subdomainarecords.example.com., ['2001:2001:2001:2001:2001:2001:2001:2001']&gt; -&gt;
*     &lt;AaaaRecord AAAA 300, subdomainarecords.example.com., ['4001:4001:4001:4001:4001:4001:4001:4001']&gt; (config)
*   Update
*     &lt;ARecord A 300, subdomainarecords.example.com., ['100.100.100.100']&gt; -&gt;
*     &lt;ARecord A 300, subdomainarecords.example.com., ['300.300.300.300']&gt; (config)
*   Summary: Creates=0, Updates=2, Deletes=0, Existing Records=6
********************************************************************************
</code></pre></div></div>

<p>Now, let’s apply our changes! To do this, add the <code class="language-plaintext highlighter-rouge">--doit</code> flag to our <code class="language-plaintext highlighter-rouge">octodns-sync</code> command:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gp">$</span><span class="w"> </span>docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
<span class="go">    -e CLOUDFLARE_EMAIL=yourlogin@gmail.com \
    -e CLOUDFLARE_TOKEN=yourtoken \
</span><span class="gp">    -v $</span><span class="o">(</span><span class="nb">pwd</span><span class="o">)</span>:/opt/dns <span class="se">\</span>
<span class="go">    parkr/octodns:v0.9.4 \
    octodns-sync --config-file /opt/dns/config/production.yaml --doit
</span></code></pre></div></div>

<p>You’ll see <code class="language-plaintext highlighter-rouge">CloudflareProvider[cloudflare] apply: making changes</code> when it’s making your changes.</p>

<p>Tada! Wait for the records to expire (300 seconds) and <code class="language-plaintext highlighter-rouge">dig</code> for your new records!</p>

<h3 id="4-make-it-go-with-github-flow">4. Make it go with GitHub Flow</h3>

<p>Now, let’s propose a change using a PR workflow. This is especially important
when you’re managing the DNS</p>

<ol>
  <li>Create a new branch and check it out</li>
  <li>Modify a record and commit the changes</li>
  <li>Push the changes and submit a PR</li>
  <li>Run octodns-sync without <code class="language-plaintext highlighter-rouge">--doit</code> and leave a comment with the output to show the changes you’re making</li>
  <li>Get approval</li>
  <li>Run octodns-sync with <code class="language-plaintext highlighter-rouge">--doit</code> to apply the changes</li>
  <li>Verify the records are present in DNS and merge the PR</li>
</ol>

<p>:tada: GitHub Flow helps make managing your DNS much easier.</p>

<h2 id="see-it-in-action">See it in action</h2>

<p>Want to see it in action? I am managing my own DNS at <a href="https://github.com/parkr/dns">parkr/dns</a>, as well as the DNS for Jekyll at <a href="https://github.com/jekyll/dns">jekyll/dns</a>.</p>

<p>I hope this helps illustrate how OctoDNS can take the pain away from managing DNS manually in a web portal.</p>

]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[1.1.1.1]]></title>
    <link href="https://byparker.com/blog/2018/1-1-1-1/"/>
    <updated>2018-04-02T14:04:22+00:00</updated>
    <id>https://byparker.com/blog/2018/1-1-1-1</id>
    <content type="html"><![CDATA[<p><a href="https://blog.cloudflare.com/announcing-1111/">Cloudflare recently announced the availability of
1.1.1.1</a>, a publicly
available, privacy-first domain name server (DNS). I have been using
OpenDNS on my routers for years and been quite happy with it. But
Cloudflare’s guarantees for logging and support for DNS over TLS and DNS
over HTTPS are definitely things I’d like to see supported, so perhaps I’ll
give them a try.</p>

<p>Their most significant claim (and the biggest reason to switch) is that
it’s much faster than other domain name servers. I live in Northern
Virginia, so my normal internet browsing is ridiculously low-latency. When
you live tens of miles from one of the largest data center hotspots in the
U.S., you can expect quick response times. If I <code class="language-plaintext highlighter-rouge">ping github.com</code> for
example, I get a response in about 5ms. OpenDNS is about ~7-8ms on average
response times. So I figured I would measure 1.1.1.1:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>64 bytes from 1.1.1.1: icmp_seq=5997 ttl=58 time=4.470 ms
64 bytes from 1.1.1.1: icmp_seq=5998 ttl=58 time=5.055 ms
64 bytes from 1.1.1.1: icmp_seq=5999 ttl=58 time=5.089 ms
64 bytes from 1.1.1.1: icmp_seq=6000 ttl=58 time=5.987 ms
^C
--- 1.1.1.1 ping statistics ---
6001 packets transmitted, 6001 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 2.496/6.336/313.273/9.857 ms
</code></pre></div></div>

<p>6.3ms on average is pretty darn good. This measurement was taken ~9-10pm on
a Sunday night, so you might imagine added latency due to streaming video.
I left this <code class="language-plaintext highlighter-rouge">ping</code> running for 1 hour and 40 minutes, and kept on using my
computer like I normally do. There were some odd spikes to 175ms, and even
one to 313ms (!), but overall the performance was stellar. 6.3ms plus or
minus 9.9ms isn’t <em>so</em> bad. (In a much shorter test, OpenDNS’s
<code class="language-plaintext highlighter-rouge">208.67.222.222</code> was 7.7ms plus or minus 11ms, so it is indeed faster).</p>

<p>Reading through some of the comments on the above blog post, you’ll see
that some folks are having issues connecting. ISPs (even Comcast in
Nashville, apparently) are blocking traffic to 1.1.1.1 (and sometimes the
alternative address 1.0.0.1), which is a bummer. YMMV, so before switching
over, be sure to test that you can connect. They also have IPv6 addresses:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ host 1dot1dot1dot1.cloudflare-dns.com
1dot1dot1dot1.cloudflare-dns.com has address 1.0.0.1
1dot1dot1dot1.cloudflare-dns.com has address 1.1.1.1
1dot1dot1dot1.cloudflare-dns.com has IPv6 address 2606:4700:4700::1001
1dot1dot1dot1.cloudflare-dns.com has IPv6 address 2606:4700:4700::1111
</code></pre></div></div>

<p>This DNS was announced yesterday and while it seems like a great addition
to the growing list of DNS alternatives, I think I’ll let others take the
leap for a few months before diving in. Shaving 1.1ms on average off my DNS
response times likely won’t make a huge difference in my day-to-day
browsing. I hope Cloudflare’s new DNS is successful, for the benefit  of
the whole internet.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Managing your personal servers with Puppet]]></title>
    <link href="https://byparker.com/blog/2018/managing-your-personal-servers-with-puppet/"/>
    <updated>2018-03-30T17:24:24+00:00</updated>
    <id>https://byparker.com/blog/2018/managing-your-personal-servers-with-puppet</id>
    <content type="html"><![CDATA[<p>At GitHub, we use Puppet to manage all our physical and virtual hosts’ configurations based on app-role combinations. Having become familiar with Puppet over my tenure there, I finally decided last month that I should be using Puppet to manage my own servers.</p>

<p>When I was just graduating from college and had a steady income, I finally decided it was time to have my own cloud server. Rackspace was the easiest choice back then, and I spun up a server with CentOS 6. Being new to server administration, I spent a long time organizing the packages and apps I installed. I ran a few Ruby web apps and hosted my static site built with Jekyll. I even went through the arduous process of manually configuring postfix to receive email for me on this host. Needless to say, I learned a lot.</p>

<p>GitHub switched to Debian in the last year and I figured I would migrate my server to Debian, too. During my internship at 6Wunderkinder / Wunderlist, I saw first-hand how nice <a href="https://web.archive.org/web/20140527014911/https://chadfowler.com/blog/2013/06/23/immutable-deployments/">immutable deploys</a> were. The concept was fairly novel for a Ruby on Rails shop: with each deploy, spin up brand new VM’s, install your code, add them to the load balancer, and spin down the old ones. This is very much how Kubernetes rolling deploys happen today. After several years of my CentOS server running and collecting cruft, I decided to follow in 6Wunderkinder’s footsteps and spun up a fresh Debian server.</p>

<p>In the interim, I had, like millions of other developers, become enammored with Docker. The idea of packaging up code and dependencies in a language-agnostic container was immediately appealing. I began experimenting with running my apps inside Docker containers, and proxying traffic from nginx to the Docker containers. After a while, I found a system I liked: run the docker container in an <a href="https://web.archive.org/web/20180304203519/https://upstart.ubuntu.com/">upstart</a> script, proxy traffic received by nginx to the docker container. When I wanted to update the app, I pushed a new revision of the Docker image, modified my upstart script to use the new tag, and ran <code class="language-plaintext highlighter-rouge">restart &lt;app&gt;</code> on my VM.</p>

<p>Over time, I made some improvements to this: I migrated all my apps from upstart to <a href="https://en.wikipedia.org/wiki/Systemd">systemd</a> when moving to Debian, and I <a href="https://github.com/parkr/nginxconf#readme">generated nginx configurations for static and proxy sites</a> instead of manually creating them.</p>

<p>In the last month or so, I decided that spending $35/mo on a VM didn’t make much sense when there were equivalent computing environments for much less money. I signed up for Linode, which offers 1GB VM’s for $5/mo, and had to migrate all my apps again. Time for a configuration management system! I had never much liked Chef, so I chose to give Puppet a try. What a massive difference this change has made.</p>

<p>I began by setting up my development environment: creating my user, adding my SSH public keys, and cloning my dotfiles. This was a fairly straight-forward process. I pushed up this new code to a repository. In order to get started with Puppet on the new host, I had to install Puppet. I went with v4.8, cloned down my repository to <code class="language-plaintext highlighter-rouge">/etc/puppet</code>, and ran <code class="language-plaintext highlighter-rouge">puppet apply</code>. It worked! I setup a <code class="language-plaintext highlighter-rouge">script/apply</code> bash script and set it up to run every hour.</p>

<p>The next piece was to setup hieradata and a <code class="language-plaintext highlighter-rouge">parker::app</code> resource which I could use to setup my above flow. First piece was the docker image. I installed the <code class="language-plaintext highlighter-rouge">puppetlabs-docker</code> Puppet module (using puppet-librarian, of course!), and used the <code class="language-plaintext highlighter-rouge">docker::image</code> resource to download the image of my choosing. Next, I created a <code class="language-plaintext highlighter-rouge">systemd::unit_file</code> for my app. The unit file is based on a common template using <code class="language-plaintext highlighter-rouge">docker run</code> and writing all environment variables to a file and using the <code class="language-plaintext highlighter-rouge">--env-file</code> flag when running the docker container. It exposes the server to a port of my choosing. I used the <code class="language-plaintext highlighter-rouge">camptocamp-systemd</code> Puppet module to get <code class="language-plaintext highlighter-rouge">systemd::systemctl::daemon_reload</code>, which I notify when a systemd unit file changes. Last, I used the <code class="language-plaintext highlighter-rouge">service</code> resource to ensure the service is running when I apply my puppet catalog.</p>

<p>This made this very easy! I setup a new <code class="language-plaintext highlighter-rouge">.pp</code> file for each of the servers I wanted to run. For those which used a database, I used the <code class="language-plaintext highlighter-rouge">puppetlabs-mysql</code> module and used the <code class="language-plaintext highlighter-rouge">mysql::db</code> resource to create a database, user, and password specific to each app. If I needed any other resources (for example, a specific unix user for the app), then I could add these in this <code class="language-plaintext highlighter-rouge">.pp</code> file.</p>

<p>To install each app, I used hieradata. Hiera is basically the greatest thing ever, and makes managing your puppet code so much nicer. All my parameters to each class go in the hiera YAML file for each host. And at the top, I specify a <code class="language-plaintext highlighter-rouge">classes</code> array, which essentially <code class="language-plaintext highlighter-rouge">include</code>s each class. When I want a new app, I create the class for it, and include it in this list for the host I want to install it on.</p>

<p>I also run a few static sites from this server (like this one!) and wanted to set those up, too. I created a site resource and created a new class for each static site I wanted to host. These are simple webroot nginx configurations so all I do is create directories, symlink a file in sites-available to sites-enabled, and let my <a href="/go/jekyll-build-server">jekyll-build-server</a> handle the creation of the site.</p>

<p>This has been working great so far! Setting up new apps and static sites is super simple now. I’m in the process of both manging nginx with this (I <a href="https://github.com/parkr/dotfiles/blob/master/bin/install-nginx">compile manually using the versions of nginx, zlib, openssl, and pcre that I want</a>), and setting up my home NUC to use this, too. I have a bunch of little command-line utilities which do my bidding and it’d be nice to get those all installed via this new method, too.</p>

<p>One open question I have is managing secrets. I just write these in plain text in my hieradata YAML file, which isn’t ideal. Anyone with the proper unix permissions who gets onto my server can see all my credentials. One thing I’ve done is setup a separate user on GitHub.com with access to my static sites and other repos of mine and use personal access tokens for that user instead of for my user. This helps reduce the likelihood that a security breach on my servers would result in any meaningful access to private code that I didn’t explicitly allow my server to access. Using GitHub Apps seems like a better next step, but I haven’t quite gotten there. If you have any recommendations for how I can better organize this, please drop me an email!</p>

<p>Managing my servers with Puppet has been a great pleasure so far and I’m really pleased with how it’s helped make managing my servers even easier. :sparkles:</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Schwartzian transform & faster sorting]]></title>
    <link href="https://byparker.com/blog/2017/schwartzian-transform-faster-sorting/"/>
    <updated>2017-09-01T16:38:47+00:00</updated>
    <id>https://byparker.com/blog/2017/schwartzian-transform-faster-sorting</id>
    <content type="html"><![CDATA[<p>In responding to a Jekyll pull request, I went digging around the way Ruby
handles sorting. The problem was that we were trying to sort a list of
objects which don’t all have a given property. The contributor was using
<code class="language-plaintext highlighter-rouge">sort_by</code> which throws an ArgumentError if the block returns a <code class="language-plaintext highlighter-rouge">nil</code> value
at all. We had a sparse property we wanted to sort by.</p>

<p>Our typical solution to this is something like:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sort_sparse_property</span><span class="p">(</span><span class="n">objects</span><span class="p">,</span> <span class="n">property</span><span class="p">)</span>
  <span class="n">objects</span><span class="p">.</span><span class="nf">sort</span> <span class="k">do</span> <span class="o">|</span><span class="n">apple</span><span class="p">,</span> <span class="n">orange</span><span class="o">|</span>
    <span class="n">apple_value</span> <span class="o">=</span> <span class="n">apple</span><span class="p">.</span><span class="nf">public_send</span><span class="p">(</span><span class="n">property</span><span class="p">)</span>
    <span class="n">orange_value</span> <span class="o">=</span> <span class="n">orange</span><span class="p">.</span><span class="nf">public_send</span><span class="p">(</span><span class="n">property</span><span class="p">)</span>

    <span class="k">if</span> <span class="o">!</span><span class="n">apple_value</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">&amp;&amp;</span> <span class="n">orange_value</span><span class="p">.</span><span class="nf">nil?</span>
      <span class="o">-</span><span class="mi">1</span>
    <span class="k">elsif</span> <span class="n">apple_value</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">orange_value</span><span class="p">.</span><span class="nf">nil?</span>
      <span class="mi">1</span>
    <span class="k">else</span>
      <span class="n">apple_value</span> <span class="o">&lt;=&gt;</span> <span class="n">orange_value</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Indeed, we did that in <code class="language-plaintext highlighter-rouge">Jekyll::Filters#sort_input</code>. It works fine, but
it’s pretty slow. The speed difference between <code class="language-plaintext highlighter-rouge">Enumerable#sort</code> and
<code class="language-plaintext highlighter-rouge">Enumerable#sort_by</code> is well-documented: <code class="language-plaintext highlighter-rouge">sort_by</code> wins every time. But
why?</p>

<p>Well, I read <a href="https://ruby-doc.org/core-2.4.1/Enumerable.html#method-i-sort_by">the documentation for <code class="language-plaintext highlighter-rouge">Enumerable#sort_by</code></a>
<strong>all the way through to the end.</strong> Lo, and behold! It walks right through
an optimization problems using <code class="language-plaintext highlighter-rouge">sort</code>! The key problem is that <strong>the <code class="language-plaintext highlighter-rouge">sort</code>
block is allocating objects during every call</strong>. That’s a <em>huge</em> waste of
allocations, and if you have ever worked on production systems written in
Ruby, you know object allocations are one of the key reasons for slowness.</p>

<p>To reduce object allocations, the Ruby documentation suggests something
called a “Schwartzian transform”:</p>

<blockquote>
  <p>A more efficient technique is to cache the sort keys (modification times in this case)
before the sort. Perl users often call this approach a Schwartzian transform, after
Randal Schwartz. We construct a temporary array, where each element is an array
containing our sort key along with the filename. We sort this array, and then extract
the filename from the result.</p>

  <p>This is exactly what <code class="language-plaintext highlighter-rouge">sort_by</code> does internally.</p>
</blockquote>

<p>Holy smokes! So going back to our example above, it’s suggesting we first
extract the property for each object, then we sort, then we pull the object
out again. Let’s give that a try:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sort_sparse_property</span><span class="p">(</span><span class="n">objects</span><span class="p">,</span> <span class="n">property</span><span class="p">)</span>
  <span class="n">objects</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">object</span><span class="o">|</span> <span class="p">[</span><span class="n">object</span><span class="p">.</span><span class="nf">public_send</span><span class="p">(</span><span class="n">property</span><span class="p">),</span> <span class="n">object</span><span class="p">]</span> <span class="p">}</span>
    <span class="p">.</span><span class="nf">sort</span> <span class="k">do</span> <span class="o">|</span><span class="n">apple</span><span class="p">,</span> <span class="n">orange</span><span class="o">|</span>
      <span class="c1"># apple &amp; orange are now an arrays of [value, object]</span>
      <span class="c1"># Use the value cached in this array to compare!</span>
      <span class="k">if</span> <span class="o">!</span><span class="n">apple</span><span class="p">.</span><span class="nf">first</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">&amp;&amp;</span> <span class="n">orange</span><span class="p">.</span><span class="nf">first</span><span class="p">.</span><span class="nf">nil?</span>
        <span class="o">-</span><span class="mi">1</span>
      <span class="k">elsif</span> <span class="n">apple</span><span class="p">.</span><span class="nf">first</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">orange</span><span class="p">.</span><span class="nf">first</span><span class="p">.</span><span class="nf">nil?</span>
        <span class="mi">1</span>
      <span class="k">else</span>
        <span class="n">apple</span><span class="p">.</span><span class="nf">first</span> <span class="o">&lt;=&gt;</span> <span class="n">orange</span><span class="p">.</span><span class="nf">first</span>
      <span class="k">end</span>
    <span class="k">end</span>
    <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">object_info</span><span class="o">|</span> <span class="n">object_info</span><span class="p">.</span><span class="nf">last</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The first call to <code class="language-plaintext highlighter-rouge">#map</code> creates an array for each object with the value we
want to compare paired with the object itself. The call to <code class="language-plaintext highlighter-rouge">#sort</code> performs
the sort as usual, but this time it’s using the cached value. The last
<code class="language-plaintext highlighter-rouge">#map</code> extracts the object from the sorted array of <code class="language-plaintext highlighter-rouge">[value, object]</code>
pairs.</p>

<p>I <a href="https://github.com/jekyll/jekyll/pull/6342">wrote a benchmark for Jekyll using <code class="language-plaintext highlighter-rouge">Jekyll::Document</code> objects</a>
and ran it for 2 properties: one which is sparsely available (i.e. only on
a few objects), and one which is more fully available (i.e. on all but a
small number of objects) and the results were shocking.</p>

<p>For a sparsely available property, the Schwartzian transform was
<strong>7-8x faster</strong>. And for the more fully available property, the Schwarzian
transform was <strong>1.75x faster</strong>. Those are monumental savings!</p>

<p>It seems like a huge waste to create a new array <em>and</em> a new object for
every item in the original Enumerable, but as we have just proven, it is
far more efficient due to the number of times the <code class="language-plaintext highlighter-rouge">sort</code> block is called.</p>

<p>Next time you implement a custom <code class="language-plaintext highlighter-rouge">sort</code> block, remember the Schwartzian
transform!</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Paralyzed]]></title>
    <link href="https://byparker.com/blog/2017/paralyzed/"/>
    <updated>2017-08-03T18:10:33+00:00</updated>
    <id>https://byparker.com/blog/2017/paralyzed</id>
    <content type="html"><![CDATA[<p>If you follow the Jekyll community, you might have noticed that I haven’t
been as active in the last year or two. Part of this is due to life events
usurping my free time, and part of this is something else entirely:
paralysis. I want to discuss today the paralysis.</p>

<p>Ask any open source maintainer what their job description is, and you’ll
likely hear about reviewing patches sent from community contributors as a
major responsibility of a maintainer. Maintainers are gatekeepers: they
decide what code makes it into the project and what code doesn’t. Part of
this is flat-out rejecting proposals (“This feature will not be accepted
because it does not fit into the philosophy of the project.”) and part of
this is working with contributors to get the functionality proposed up to
code style, functionality, and testing standards of the rest of the project.
Maintainers are making decisions non-stop about the product, style,
functionality, and testing quality of a contribution.</p>

<p><a href="https://en.wikipedia.org/wiki/Decision_fatigue">Decision fatigue</a> is
defined as:</p>

<blockquote>
  <p>the deteriorating quality of decisions made by an individual after a long
session of decision making.</p>
</blockquote>

<p>When I first heard of this, I immediately related to it. Decision fatigue
affects me <em>greatly</em> as a maintainer and has become a bigger problem for me
since I began working as a programmer full-time.</p>

<p>In college, my primary focus was on doing homework and studying. The
decisions I made were essentially around scheduling when to study. My free
time seemed endless in some ways, since I didn’t have a particularly rigid
schedule. Rehearsals, classes, studying, socializing. I wasn’t making
qualitative decisions about whether what I was learning deserved to be in
my brain; school doesn’t really give you that choice once you’ve selected
which classes to take. I worked on Jekyll in between classes and in the
evenings. It was an escape from the monotonous work of completing problem
sets.</p>

<p>After graduation, I took a great job at VSCO, a start-up near and dear to
my heart.  I had used VSCOCam since the early days of the Grid and was
really excited to be joining the start-up world. Jekyll was experiencing a
renaissance.  We were merging contributions, releasing more often, and
growing the project to meet the evolving needs of web developers. It was in
a good place, so I focused on making sure I could be successful at VSCO. As
my focus shifted, I began reviewing fewer issues and contributions and I
found myself punting on discussions all the time. I would save the link
somewhere and “get back to it later.” Issues and pull requests started
hanging for weeks, then months. It was getting harder and harder to
determine the merits of a contribution. Every holiday, when I had a few
days off work, reviews would start feeling a bit easier and I would chug
through some contributions, feeling guilty that I had let them stall for so
long. Once I met my girlfriend, my holidays were spent with her instead of
on my computer. Slowly, I spent less and less time on Jekyll.</p>

<p>When I began working at GitHub on Pages, we were turning Ben Balter’s 20%
project back into a full product with a full-time staff. We were cleaning
up old technical debt, we made massive architectural changes, and started
working on a roadmap for new features. I had been hired with the
understanding that I’d be able to dedicate some of my time to Jekyll. It
was an integral part of Pages after all, and I could continue building on
the renaissance with more resources behind me. I put some more work into
Jekyll, but the problems I could solve within GitHub beckoned. <a href="https://brad.livejournal.com/2409049.html">Brad
Fitzpatrick wrote about this about people joining
Google</a>:</p>

<blockquote>
  <p>Prior to joining Google I always joked that Google was the black hole
that swallowed up open source programmers. I’d see awesome, productive
hackers join Google and then hear little to nothing from them afterwards.
… Many open source programmers are just programmers. They like working
on fun, hard problems, whether on open source or otherwise.  … The
Google development environment is so nice. … It’s very easy to hack on
anything, anywhere and submit patches to anybody …</p>
</blockquote>

<p>All this goes to say: fun, hard, <em>new-to-me</em> problems existed at work and I
got to work on them with some of the best minds in the start-up world. The
development environment at GitHub is <em>really</em> nice. Building a feature into
the main app is easy, testing is full of niceties I never had elsewhere,
and deployment is just 1 command to Hubot. I began to slowly disappear from
Jekyll into this wonderland of Ruby and smart people at GitHub. When I was
in college, I didn’t know a single person who was familiar with the
command-line until Senior year. Students learn to program Java &amp; Python in
IDE’s, and even our classes about net infrastructure revolved around the
AWS web UI. Joining the Jekyll community my Freshman year helped me learn
practical development skills (like source control!) from really smart
people. GitHub was that, but full-time, Chat, and great centralized tooling.
The things I had joined the Jekyll community to learn I had learned, and
was spending all my time soaking up the nuggets of wisdom from my
colleagues, disappearing from the open source community for long lengths of
time.</p>

<p>Some wisdom I have picked up has come in the form of advice from Ben Balter
and Mike McQuaid pertaining to running an open source community. Ben had
the idea of <a href="https://jekyllrb.com/news/2016/03/10/making-it-easier-to-contribute-to-jekyll/">affinity teams &amp; affinity team
captains</a>
and the idea of setting up <a href="https://jekyllrb.com/philosophy">a philosophy
document</a> as guiding principles for the
development of Jekyll. Mike suggested I write maintainer-focused
documentation for existing and future Jekyll maintainers. <a href="https://jekyllrb.com/docs/maintaining/">I did that
too</a>. Clearly, communication and
delegation were two aspects of running an open source community that I
still hadn’t learned well.</p>

<p>Every time I take a look at the list of PR’s, I want to just
close the vast majority of them because they don’t seem useful to me. I
have lost my broader lens of users’ needs, and we haven’t documented our
core use-cases anywhere. When I see a contribution, I don’t know how to
evaulate it. We have <a href="https://jekyllrb.com/docs/maintaining/reviewing-a-pull-request/">process
docs</a>, but
nothing stating how to evaluate feature requests. Do you ask the user to
ship it as a plugin? How do you know if it should be in Jekyll core? Should
<em>we</em> ship a new plugin with this functionality? Does this break a relied-on
behavior? This laundry-list of questions spirals out of control until I
close my browser tab.</p>

<p>Recently, I have been considering what it would look like to pass on the
torch of lead maintainer to another member of the Jekyll community. Our
affinity team captains are really excellent and are passionate about making
Jekyll great. But I’m afraid if I leave, the new maintainers won’t feel
like they can make decisions because they’ll feel the same paralysis.</p>

<p>Is this paralysis, coupled with the pressure of a project that needs work
if it will survive, the first step in burning out?</p>

<p>I don’t know the answers here, and I don’t know when I’ll find them. All I
know is that I need to start asking myself how I can reduce decision
fatigue. The Jekyll community deserves better than an absentee lead
maintainer. If you have any ideas or resources for dealing with decision
fatigue, feel free to email me. Or, if you also suffer from this and just
want to reach out to commiserate, I’m all ears. I’d be really curious to
hear your thoughts.</p>
]]></content>
  </entry>
  
</feed>
