<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>GitHub on Redowan&#39;s Reflections</title>
    <link>https://rednafi.com/tags/github/</link>
    <description>Recent content in GitHub on Redowan&#39;s Reflections</description>
    <image>
      <title>Redowan&#39;s Reflections</title>
      <url>https://blob.rednafi.com/static/images/home/cover.png</url>
      <link>https://blob.rednafi.com/static/images/home/cover.png</link>
    </image>
    <generator>Hugo -- 0.154.2</generator>
    <language>en</language>
    <lastBuildDate>Fri, 13 Mar 2026 11:58:57 +0100</lastBuildDate>
    <atom:link href="https://rednafi.com/tags/github/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Here-doc headache</title>
      <link>https://rednafi.com/misc/heredoc-headache/</link>
      <pubDate>Fri, 19 Jul 2024 00:00:00 +0000</pubDate>
      <guid>https://rednafi.com/misc/heredoc-headache/</guid>
      <description>Avoid here-doc pitfalls when running remote commands via SSH. Learn variable expansion gotchas and simpler alternatives for deployment scripts.</description>
      <category>Shell</category>
      <category>DevOps</category>
      <category>GitHub</category>
      <content:encoded><![CDATA[<p>I was working on the deployment pipeline for a service that launches an app in a dedicated
VM using GitHub Actions. In the last step of the workflow, the CI SSHs into the VM and runs
several commands using a <a href="https://tldp.org/LDP/abs/html/here-docs.html" rel="noopener noreferrer" target="_blank">here document</a> in bash. The simplified version looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># SSH into the remote machine and run commands to deploy the service</span>
</span></span><span class="line"><span class="cl">ssh <span class="nv">$SSH_USER</span>@<span class="nv">$SSH_HOST</span> <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="cl"><span class="s">    # Go to the work directory
</span></span></span><span class="line"><span class="cl"><span class="s">    cd $WORK_DIR
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">    # Make a git pull
</span></span></span><span class="line"><span class="cl"><span class="s">    git pull
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">    # Export environment variables required for the service to run
</span></span></span><span class="line"><span class="cl"><span class="s">    export AUTH_TOKEN=$APP_AUTH_TOKEN
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">    # Start the service
</span></span></span><span class="line"><span class="cl"><span class="s">    docker compose up -d --build
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>The fully working version can be found in the <a href="https://github.com/rednafi/serve-init/blob/7232c55c9aa3a6c34c5da6aeb9d14afc88d9aa0e/.github/workflows/ci.yml#L86-L115" rel="noopener noreferrer" target="_blank">serve-init repo with here-doc</a>.</p>
<p>Here, environment variables like <code>SSH_USER</code>, <code>SSH_HOST</code>, and <code>APP_AUTH_TOKEN</code> are defined in
the surrounding local scope of the CI. The variables then get propagated to the remote
machine when we run the commands via here-doc.</p>
<p>However, I couldn&rsquo;t figure out why the Docker containers weren&rsquo;t able to access the value of
the <code>AUTH_TOKEN</code> variable. The other variables were getting through just fine.</p>
<p>It turns out, <code>export AUTH_TOKEN=$AUTH_TOKEN</code> within the here-doc block, doesn&rsquo;t export the
variable in the remote shell. So this doesn&rsquo;t do what I thought it would:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="cl"><span class="s">    export FOO=bar
</span></span></span><span class="line"><span class="cl"><span class="s">    echo $FOO
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>I was expecting it to print:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">export FOO=bar
</span></span><span class="line"><span class="cl">echo bar
</span></span></code></pre></div><p>But instead, it just prints:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">export FOO=bar
</span></span><span class="line"><span class="cl">echo
</span></span></code></pre></div><p>So <code>export FOO=bar</code> in the here-doc block doesn&rsquo;t set the variable in the remote shell. One
solution is to set it before the block like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">FOO</span><span class="o">=</span>bar
</span></span><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="cl"><span class="s">    echo $FOO
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>This prints:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">echo bar
</span></span></code></pre></div><p>So, in the CI pipeline, we could do the following to propagate the environment variable from
local to the remote machine:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">FOO</span><span class="o">=</span>bar
</span></span><span class="line"><span class="cl">ssh <span class="nv">$SSH_USER</span>@<span class="nv">$SSH_HOST</span> <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="cl"><span class="s">    echo $FOO
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>This will print the value of the environment variable on the remote machine correctly.
However, this doesn&rsquo;t set the value in the remote shell&rsquo;s environment. If you SSH into the
remote machine and try to print the variable&rsquo;s value, you&rsquo;ll see nothing gets printed. The
previous command only passes the value to the remote machine temporarily and doesn&rsquo;t set it
permanently in the remote shell.</p>
<p>To fix it, you could pipe the value into a file and load it in the remote shell like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">ssh <span class="nv">$SSH_USER</span>@<span class="nv">$SSH_HOST</span> <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="cl"><span class="s">    echo &#34;export FOO=$FOO&#34; &gt; /tmp/.env
</span></span></span><span class="line"><span class="cl"><span class="s">    source /tmp/.env
</span></span></span><span class="line"><span class="cl"><span class="s">    echo \$FOO
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>Here, <code>echo \$FOO</code> instead of <code>echo $FOO</code> ensures that the shell expansion is done on the
remote machine, not on the local. This allows us to know that the environment variable has
been set in the remote shell correctly.</p>
<p>Maybe the behavior makes sense, but it still broke my mental model.</p>
<p>So I decided to get rid of here-doc in the pipeline altogether and went with this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nv">SCRIPT</span><span class="o">=</span><span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    # Go to the work directory
</span></span></span><span class="line"><span class="cl"><span class="s2">    cd </span><span class="nv">$WORK_DIR</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    # Make a git pull
</span></span></span><span class="line"><span class="cl"><span class="s2">    git pull
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    # Export environment variables required for the service to run
</span></span></span><span class="line"><span class="cl"><span class="s2">    export AUTH_TOKEN=</span><span class="nv">$APP_AUTH_TOKEN</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    # Start the service
</span></span></span><span class="line"><span class="cl"><span class="s2">    docker compose up -d --build
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run the script on the remote machine</span>
</span></span><span class="line"><span class="cl">ssh <span class="nv">$SSH_USER</span>@<span class="nv">$SSH_HOST</span> <span class="s2">&#34;</span><span class="nv">$SCRIPT</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>It <a href="https://github.com/rednafi/serve-init/blob/54b9b0fc94030eb4b9749fd4a5823a8867545f6a/.github/workflows/ci.yml#L86-L113" rel="noopener noreferrer" target="_blank">works without here-doc</a>!</p>
<p>One thing to keep in mind with the second approach is that if you need to run any expanded
commands, you&rsquo;ll need to defer it with a backslash so that it&rsquo;s run on the remote machine,
not on the local:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nv">SCRIPT</span><span class="o">=</span><span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    # ...
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">    # Without backslash, shell runs this locally
</span></span></span><span class="line"><span class="cl"><span class="s2">    docker rmi -f \$(docker compose images -q) || true
</span></span></span><span class="line"><span class="cl"><span class="s2">    &#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Run the script on the remote machine</span>
</span></span><span class="line"><span class="cl">ssh <span class="nv">$SSH_USER</span>@<span class="nv">$SSH_HOST</span> <span class="s2">&#34;</span><span class="nv">$SCRIPT</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>Without the backslash, the <code>$(...)</code> will be expanded on the local machine, which is not
desirable here. The backslash defers it so that it runs on the remote instead.</p>
<!-- references -->
<!-- prettier-ignore-start -->
<!-- prettier-ignore-end -->
]]></content:encoded>
    </item>
    <item>
      <title>The sane pull request</title>
      <link>https://rednafi.com/misc/sane-pull-request/</link>
      <pubDate>Sun, 14 Jul 2024 00:00:00 +0000</pubDate>
      <guid>https://rednafi.com/misc/sane-pull-request/</guid>
      <description>Make pull requests easier to review. Learn commit organization, diff filtering, annotations, and context that helps reviewers understand changes faster.</description>
      <category>Git</category>
      <category>GitHub</category>
      <content:encoded><![CDATA[<p>One of the reasons why I&rsquo;m a big advocate of rebasing and cleaning up feature branches, even
when the changes get squash-merged to the mainline, is that it makes the PR reviewer&rsquo;s life
a little easier. I&rsquo;ve written about <a href="/misc/on-rebasing/">my rebasing workflow</a> before and learned a few new
things from the <a href="https://news.ycombinator.com/item?id=40742628" rel="noopener noreferrer" target="_blank">Hacker News discussion</a> around it.</p>
<p>While there&rsquo;s been no shortage of text on why and how to craft <a href="https://www.aleksandrhovhannisyan.com/blog/atomic-git-commits/" rel="noopener noreferrer" target="_blank">atomic commits</a>, I often
find those discussions focus too much on VCS hygiene, and the main benefit gets lost in the
minutiae. When working in a team setup, I&rsquo;ve discovered that individual commits matter much
less than the final change list.</p>
<p>Also, I find some of the prescriptive suggestions for easier review, like keeping the PR
under ~150 lines, ensuring that the tests pass in each commit, and tidying the commits to be
strictly independent, quite cumbersome. <a href="https://benjamincongdon.me/blog/2022/07/17/In-Praise-of-Stacked-PRs/" rel="noopener noreferrer" target="_blank">Stacked PRs</a> sometimes help to make large changes a
bit more tractable, but that comes with a whole set of review-conflict-feedback challenges.
So this piece will mainly focus on making large PRs a wee bit easier to work with.</p>
<p>Here&rsquo;s a quick rundown of the things I find useful to make reviewing the grunt work of pull
requests a bit more tractable. I don&rsquo;t always strictly follow them while doing personal or
OSS work, but these steps have been helpful while working on a large shared repo at work.</p>
<ul>
<li>
<p>Avoiding the temptation to lump tangentially related changes into a PR to speed things up.</p>
</li>
<li>
<p>Having a ton of fragmented commits makes filtering useless when navigating the PR diff in
a platform like GitHub. I really like to filter diffs on GitHub, but it wouldn&rsquo;t be useful
if the commits are all over the place.</p>
<p><img alt="GitHub PR diff view with commit filter dropdown for navigating changes" loading="lazy" src="https://blob.rednafi.com/static/images/sane_pull_request/img_1.png"></p>
</li>
<li>
<p>To make diff filtering better, I often rebase my feature branch after a messy development
workflow and divide the changes into a few commits clustered around the core
implementation, tests, documentation, dependency upgrades, and occasional refactoring.</p>
</li>
<li>
<p>Rebasing all the changes into a single commit is okay if the change is small, but for
bigger changes, this does more harm than good.</p>
</li>
<li>
<p>I&rsquo;ve rarely spent the time to ensure that the individual commits are <a href="https://simonwillison.net/2022/Oct/29/the-perfect-commit/" rel="noopener noreferrer" target="_blank">perfect</a> - in the
sense that they&rsquo;re complete with passing tests or documentation. As long as the complete
change list makes sense as a whole, it&rsquo;s good enough. YMMV. The main goal is to make sure
the diff makes sense to the person reviewing the work.</p>
</li>
<li>
<p>Annotated comments from the author on the PR are great. I wish they&rsquo;d take up less space
and there was a way to collapse them individually.</p>
<p><img alt="GitHub PR with author annotations explaining code changes inline" loading="lazy" src="https://blob.rednafi.com/static/images/sane_pull_request/img_2.png"></p>
</li>
<li>
<p>Each PR must be connected to either an Issue or a Jira ticket, depending on how the team
works.</p>
</li>
<li>
<p>Adding context, screenshots, gifs, and videos to the PR description makes things so much
easier for me when I do the review. Being able to see that the changes work as intended
without running the code has its benefits.</p>
<p><img alt="PR description with embedded screenshot demonstrating feature behavior" loading="lazy" src="https://blob.rednafi.com/static/images/sane_pull_request/img_3.png"></p>
</li>
<li>
<p>Keeping the PR in draft state until it&rsquo;s ready to be reviewed. I&rsquo;m not a fan of getting a
notification to review some work only to find that it&rsquo;s not ready yet.</p>
</li>
</ul>
<!-- references -->
<!-- prettier-ignore-start -->
<!-- prettier-ignore-end -->
]]></content:encoded>
    </item>
    <item>
      <title>I kind of like rebasing</title>
      <link>https://rednafi.com/misc/on-rebasing/</link>
      <pubDate>Tue, 18 Jun 2024 00:00:00 +0000</pubDate>
      <guid>https://rednafi.com/misc/on-rebasing/</guid>
      <description>Master git rebase for cleaner commit history. Learn interactive rebasing, squashing commits, and rebasing feature branches onto main with practical examples.</description>
      <category>git</category>
      <category>Shell</category>
      <category>GitHub</category>
      <content:encoded><![CDATA[<p>People tend to get pretty passionate about Git workflows on different online forums. Some
like to rebase, while others prefer to keep the disorganized records. Some dislike the extra
merge commit, while others love to preserve all the historical artifacts. There&rsquo;s merit to
both sides of the discussion. That being said, I kind of like rebasing because I&rsquo;m a messy
committer who:</p>
<ul>
<li>Usually doesn&rsquo;t care for keeping <a href="https://suchdevblog.com/lessons/AtomicGitCommits.html#why-should-you-write-atomic-git-commits" rel="noopener noreferrer" target="_blank">atomic commits</a>.</li>
<li>Creates a lot of short commits with messages like &ldquo;fix&rdquo; or &ldquo;wip&rdquo;.</li>
<li>Likes to clean up the untidy commits before sending the branch for peer review.</li>
<li>Prefers a linear history over a forked one so that <code>git log --oneline --graph</code> tells a
nice story.</li>
</ul>
<p>Git rebase allows me to squash my disordered commits into a neat little one, which bundles
all the changes with passing tests and documentation. Sure, a similar result can be emulated
using <code>git merge --squash feat_branch</code> or GitHub&rsquo;s squash-merge feature, but to me, rebasing
feels cleaner. Plus, over time, I&rsquo;ve subconsciously picked up the tricks to work my way
around rebase-related gotchas.</p>
<p>Julia Evans explores the <a href="https://jvns.ca/blog/2023/11/06/rebasing-what-can-go-wrong-/" rel="noopener noreferrer" target="_blank">pros and cons of rebasing</a> in detail. Also, squashing commits is
just one of the many things that you can do with the rebase command. Here, I just wanted to
document my daily rebasing workflow where I mostly rename, squash, or fixup commits.</p>
<h2 id="a-few-assumptions">A few assumptions</h2>
<p>Broadly speaking, there are two common types of rebasing: rebasing a feature branch onto the
main branch and interactive rebasing on the feature branch itself. The workflow assumes a
usual web service development cadence where:</p>
<ul>
<li>You&rsquo;ll be working on a feature branch that&rsquo;s forked off of a main branch.</li>
<li>The main branch is protected, and you can&rsquo;t directly push your changes to it.</li>
<li>Once you&rsquo;re done with your feature work, you&rsquo;ll need to create a pull request against the
main branch.</li>
<li>After your PR is reviewed and merged onto the main branch, CI automatically deploys it to
some staging environment.</li>
</ul>
<p>I&rsquo;m aware this approach doesn&rsquo;t work for some niches in software development, but it&rsquo;s the
one I&rsquo;m most familiar with, so I&rsquo;ll go with it.</p>
<h2 id="rebasing-a-feature-branch-onto-the-main-branch">Rebasing a feature branch onto the main branch</h2>
<p>Let&rsquo;s say I want to start working on a new feature. Here&rsquo;s how I usually go about it:</p>
<ol>
<li>
<p>Pull in the latest <code>main</code> with <code>git pull</code>.</p>
</li>
<li>
<p>Fork off a new branch via <code>git switch -c feat_branch</code>.</p>
</li>
<li>
<p>Do the work in <code>feat_branch</code>, and before sending the PR, do interactive rebasing if
necessary, and then rebase the <code>feat_branch</code> onto the latest changes of <code>main</code> with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git pull --rebase origin main
</span></span></code></pre></div></li>
<li>
<p>Push the changes to the remote repository with <code>git push origin HEAD</code> and send a PR
against <code>main</code> for review.</p>
<p>Here, <code>...origin HEAD</code> instructs git to push the current branch that HEAD is pointing
to.</p>
</li>
</ol>
<p>The 3rd step is where I often do interactive rebasing before sending the PR to make my work
presentable. The next section will explain that in detail.</p>
<p>Occasionally, the 4th step doesn&rsquo;t go as expected, and merge conflicts occur when I run
<code>git rebase main</code> from <code>feat_branch</code>. In those cases, I use my editor (VSCode) to fix the
conflict, add the changes with <code>git add .</code>, and run <code>git rebase --continue</code>. This completes
the rebase operation, and we&rsquo;re ready to push it to the remote.</p>
<h2 id="rebasing-interactively-on-the-feature-branch">Rebasing interactively on the feature branch</h2>
<p>This is an extension of the 3rd step of the previous section. Sometimes, while working on a
feature, I quickly make many messy commits and push them to the remote branch. This happens
quite frequently when I&rsquo;m prototyping on a feature or updating something regarding GitHub
Actions. In these cases, I tend to make quick changes, commit with a message like &ldquo;fix&rdquo; or
&ldquo;ci&rdquo; and push to remote to see if the CI is passing. However, once I&rsquo;m done, the commit log
on that branch looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git log @ ^main --oneline --graph
</span></span></code></pre></div><p>This command instructs git to show only the commits that exist on <code>feat_branch</code> but not on
<code>main</code>. I learned recently that in git&rsquo;s context, <code>@</code> indicates the current branch. Neat,
this means I won&rsquo;t need to remember the branch name or do a <code>git branch</code> and then copy the
name of the current branch. Running the command returns:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">* 148934c (HEAD -&gt; feat_branch) ci
</span></span><span class="line"><span class="cl">* e0f6152 ci
</span></span><span class="line"><span class="cl">* 8f4dc4c ci
</span></span><span class="line"><span class="cl">* bf33bf7 ci
</span></span><span class="line"><span class="cl">* 2e3dce6 ci
</span></span></code></pre></div><p>I&rsquo;m not too proud of the state of this <code>feat_branch</code> and prefer to tidy things up before
making a PR against <code>main</code>. One common thing I do is squash all these commits into one and
then add a proper commit message. Interactive rebasing allows me to do that. Let&rsquo;s say you
want to interactively rebase the 5 commits listed above and squash them. To do so, you can
run the following command from the <code>feat_branch</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git rebase -i HEAD~5
</span></span></code></pre></div><p>This will open a file named <code>git-rebase-todo</code> in your default git editor (set via git
config) that looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">pick 763e178 ci # empty
</span></span><span class="line"><span class="cl">pick 4b10faf ci # empty
</span></span><span class="line"><span class="cl">pick 7f7ce20 ci # empty
</span></span><span class="line"><span class="cl">pick 88fc529 ci # empty
</span></span><span class="line"><span class="cl">pick 8bc19b6 ci # empty
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Rebase a2e45d3..8bc19b6 onto a2e45d3 (5 commands)
</span></span><span class="line"><span class="cl">#
</span></span><span class="line"><span class="cl"># Commands:
</span></span><span class="line"><span class="cl"># p, pick &lt;commit&gt; = use commit
</span></span><span class="line"><span class="cl"># r, reword &lt;commit&gt; = use commit, but edit the commit message
</span></span><span class="line"><span class="cl"># e, edit &lt;commit&gt; = use commit, but stop for amending
</span></span><span class="line"><span class="cl"># s, squash &lt;commit&gt; = use commit, but meld into previous commit
</span></span><span class="line"><span class="cl"># f, fixup [-C | -c] &lt;commit&gt; = like &#34;squash&#34; but keep only the previous
</span></span><span class="line"><span class="cl">#                    commit&#39;s log message, unless -C is used, in which case
</span></span><span class="line"><span class="cl">#                    keep only this commit&#39;s message; -c is same as -C but
</span></span><span class="line"><span class="cl">#                    opens the editor
</span></span><span class="line"><span class="cl"># x, exec &lt;command&gt; = run command (the rest of the line) using shell
</span></span><span class="line"><span class="cl"># b, break = stop here (continue rebase later with &#39;git rebase --continue&#39;)
</span></span><span class="line"><span class="cl"># d, drop &lt;commit&gt; = remove commit
</span></span><span class="line"><span class="cl"># l, label &lt;label&gt; = label current HEAD with a name
</span></span><span class="line"><span class="cl"># t, reset &lt;label&gt; = reset HEAD to a label
</span></span><span class="line"><span class="cl"># m, merge [-C &lt;commit&gt; | -c &lt;commit&gt;] &lt;label&gt; [# &lt;oneline&gt;]
</span></span><span class="line"><span class="cl">#         create a merge commit using the original merge commit&#39;s
</span></span><span class="line"><span class="cl">#         message (or the oneline, if no original merge commit was
</span></span><span class="line"><span class="cl">#         specified); use -c &lt;commit&gt; to reword the commit message
</span></span><span class="line"><span class="cl"># u, update-ref &lt;ref&gt; = track a placeholder for the &lt;ref&gt; to be updated
</span></span><span class="line"><span class="cl">#                       to this position in the new commits. The &lt;ref&gt; is
</span></span><span class="line"><span class="cl">#                       updated at the end of the rebase
</span></span><span class="line"><span class="cl">#
</span></span><span class="line"><span class="cl"># These lines can be re-ordered; they are executed from top to bottom.
</span></span><span class="line"><span class="cl">#
</span></span><span class="line"><span class="cl"># If you remove a line here THAT COMMIT WILL BE LOST.
</span></span><span class="line"><span class="cl">#
</span></span><span class="line"><span class="cl"># However, if you remove everything, the rebase will be aborted.
</span></span><span class="line"><span class="cl">#
</span></span></code></pre></div><p>Notice that the file has quite a bit of instructions that are commented out. You can perform
actions like pick, reword, edit, fixup, etc. I usually use squash and edit the
<code>git-rebase-todo</code> file like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">pick 763e178 ci # empty
</span></span><span class="line"><span class="cl">s 4b10faf ci # empty  # &lt;- s=squash melds into prev commit
</span></span><span class="line"><span class="cl">s 7f7ce20 ci # empty
</span></span><span class="line"><span class="cl">s 88fc529 ci # empty
</span></span><span class="line"><span class="cl">s 8bc19b6 ci # empty
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># ... rest of the file remains untouched
</span></span></code></pre></div><p>Now, if you close the previous file, git will automatically open another file like the
following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl"># This is a combination of 5 commits.
</span></span><span class="line"><span class="cl"># This is the 1st commit message:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ci
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># This is the commit message #2:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ci
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># This is the commit message #3:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ci
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># This is the commit message #4:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ci
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># This is the commit message #5:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ci
</span></span></code></pre></div><p>After the first comment, you can put in the message for all the combined commits:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl"># This is a combination of 5 commits.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Add pip caching to the CI      # &lt;- message for the combined commits
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># ... you can remove rest of the content
</span></span></code></pre></div><p>If you close this file, you&rsquo;ll see a message on your console indicating that the rebase has
been successful:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">[detached HEAD 28f5084] Add pip caching to the CI
</span></span><span class="line"><span class="cl"> Date: Wed Jun 19 22:42:07 2024 +0200
</span></span><span class="line"><span class="cl">Successfully rebased and updated refs/heads/feat_branch.
</span></span></code></pre></div><p>Now running <code>git log</code> will show that the messy commit has been squashed into one.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git log @ ^main --oneline --graph
</span></span></code></pre></div><p>This displays:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">* 28f5084 (HEAD -&gt; feat_branch) Add pip caching to the CI
</span></span></code></pre></div><p>This is just one of the many things you can do during interactive rebasing. While I do this
most commonly, sometimes I also drop unnecessary commits to tidy up things and group
multiple commits instead of just squashing everything into one commit. All of these actions
can be done in a similar manner to squashing commits as mentioned above.</p>
<p>Sometimes, I don&rsquo;t know how many commits I&rsquo;ll need to interactively rebase. In those cases,
I can get the number of all the new commits on a feature branch by counting the entries in
<code>git log</code> as follows:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git log @ ^main --oneline <span class="p">|</span> wc -l
</span></span></code></pre></div><p>Then you can use the number from the output of the previous command to rebase <code>n</code> number of
commits:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git rebase -i HEAD~n
</span></span></code></pre></div><p>Another thing you can do is split a single commit into multiple commits. This is quite a bit
more involved and I rarely do it during interactive rebasing.</p>
<p>One last thing I learned recently is that you can run your tests or any arbitrary command
during interactive rebasing. To do so, start your rebase session with <code>--exec cmd</code> as
follows:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git rebase -i --exec <span class="s2">&#34;echo hello&#34;</span> HEAD~5
</span></span></code></pre></div><p>In the <code>git-rebase-todo</code> file this time, you&rsquo;ll see that the command is run after each
commit as follows:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">pick dffb3c1 ci # empty
</span></span><span class="line"><span class="cl">exec echo hello
</span></span><span class="line"><span class="cl">pick 4d2fa08 ci # empty
</span></span><span class="line"><span class="cl">exec echo hello
</span></span><span class="line"><span class="cl">pick 2b35e4f ci # empty
</span></span><span class="line"><span class="cl">exec echo hello
</span></span><span class="line"><span class="cl">pick 6de7a52 ci # empty
</span></span><span class="line"><span class="cl">exec echo hello
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># ...
</span></span></code></pre></div><p>You can edit this file to run the exec command after any commit you want to. The commands
will run once you save and close this file. This is a neat way to run your test suite and
make sure they pass in the intermediate commits.</p>
<p>Fin!</p>
<h2 id="further-reading">Further reading</h2>
<ul>
<li><a href="https://news.ycombinator.com/item?id=40742628" rel="noopener noreferrer" target="_blank">Hackernews discussion on rebasing</a></li>
</ul>
<!-- references -->
<!-- prettier-ignore-start -->
<!-- prettier-ignore-end -->
]]></content:encoded>
    </item>
    <item>
      <title>Building a CORS proxy with Cloudflare Workers</title>
      <link>https://rednafi.com/javascript/cors-proxy-with-cloudflare-workers/</link>
      <pubDate>Sun, 21 May 2023 00:00:00 +0000</pubDate>
      <guid>https://rednafi.com/javascript/cors-proxy-with-cloudflare-workers/</guid>
      <description>Build your own CORS proxy using Cloudflare Workers to bypass browser CORS restrictions. Includes deployment with GitHub Actions automation.</description>
      <category>JavaScript</category>
      <category>Networking</category>
      <category>GitHub</category>
      <content:encoded><![CDATA[<p>Cloudflare absolutely nailed the serverless function DX with <a href="https://workers.cloudflare.com/" rel="noopener noreferrer" target="_blank">Cloudflare Workers</a>. However,
I feel like it&rsquo;s yet to receive widespread popularity like AWS Lambda since as of now, the
service only offers a single runtime - JavaScript. But if you can look past that big folly,
it&rsquo;s a delightful piece of tech to work with. I&rsquo;ve been building small tools with it for a
couple of years but never got around to writing about the immense productivity boost it
usually gives me whenever I need to quickly build and deploy a self-contained service.</p>
<p>Recently, I was doing some lightweight frontend work and needed to make some AJAX calls from
one domain to another. Usually, browser&rsquo;s <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" rel="noopener noreferrer" target="_blank">CORS</a> (Cross-Origin Resource Sharing) policy will
get in your way if you try this. While you&rsquo;re reading this piece, open the dev console and
paste the following <code>fetch</code> snippet:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">fetch</span><span class="p">(</span><span class="s2">&#34;https://mozilla.org&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">response</span><span class="p">.</span><span class="nx">text</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Do something with the received data
</span></span></span><span class="line"><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Handle any errors that occurred during the request
</span></span></span><span class="line"><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s2">&#34;Error:&#34;</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span></code></pre></div><p>This snippet will attempt to make a <code>GET</code> request from <a href="https://rednafi.com">https://rednafi.com</a> to
<a href="https://mozilla.org" rel="noopener noreferrer" target="_blank">https://mozilla.org</a>. However, the client&rsquo;s CORS policy won&rsquo;t allow you to make an AJAX
request like this and load external resources into the current site. On your console, you&rsquo;ll
see an error message like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">Access to fetch at &#39;https://mozilla.org/&#39; from origin &#39;https://rednafi.com&#39;
</span></span><span class="line"><span class="cl">has been blocked by CORS policy: No &#39;Access-Control-Allow-Origin&#39; header is
</span></span><span class="line"><span class="cl">present on the requested resource.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">If an opaque response serves your needs, set the request&#39;s mode to
</span></span><span class="line"><span class="cl">&#39;no-cors&#39; to fetch the resource with CORS disabled.
</span></span></code></pre></div><p>This is a good security measure. Without CORS, a malicious script could make a request to a
server in another domain and access the resources that the user of the page is not intended
to have access to. So much has been said and written about CORS that I won&rsquo;t even attempt to
explain it here. Here&rsquo;s another <a href="https://medium.com/bigcommerce-developer-blog/lets-talk-about-cors-84800c726919" rel="noopener noreferrer" target="_blank">high-level introduction to CORS</a>.</p>
<h2 id="cors-proxy">CORS proxy</h2>
<p>While CORS is generally a good thing, it can be quite annoying when you&rsquo;re trying to build
something that needs access to external resources. In those cases, you&rsquo;ll have to mess
around with the origin server and add a few headers that the browser can understand before
it allows you to load those external resources. But sometimes you don&rsquo;t have access to the
origin server or simply don&rsquo;t want to deal with modifying the server&rsquo;s response headers
every time you need to access external resources. That&rsquo;s where CORS proxies can come in
handy.</p>
<blockquote>
  <p>A CORS proxy server acts as a bridge between your client and the target server. It
receives your request and forwards it to the target server with a modified origin header
so that the target server thinks the request is coming from the same origin as itself.</p>

</blockquote><p>This way, you can bypass the same-origin policy of browsers and access resources from
different domains. I usually use free proxies like <a href="https://cors.sh/" rel="noopener noreferrer" target="_blank">cors.sh</a> to bypass CORS restrictions.
You can drop this snippet to your browser&rsquo;s console and this time it&rsquo;ll allow you to load
the contents of <a href="https://mozilla.org" rel="noopener noreferrer" target="_blank">https://mozilla.org</a> from <a href="https://rednafi.com">https://rednafi.com</a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="c1">// Notice how we&#39;re prepending CORS URL before the target URL
</span></span></span><span class="line"><span class="cl"><span class="nx">fetch</span><span class="p">(</span><span class="s2">&#34;https://proxy.cors.sh/https://mozilla.org&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">response</span><span class="p">.</span><span class="nx">text</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Do something with the received data
</span></span></span><span class="line"><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Handle any errors that occurred during the request
</span></span></span><span class="line"><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s2">&#34;Error:&#34;</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span></code></pre></div><p>The target server&rsquo;s response looks somewhat like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="cp">&lt;!doctype html&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">html</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;windows no-js&#34;</span> <span class="na">lang</span><span class="o">=</span><span class="s">&#34;en&#34;</span> <span class="na">dir</span><span class="o">=</span><span class="s">&#34;ltr&#34;</span> <span class="na">data-country-code</span><span class="o">=</span><span class="s">&#34;US&#34;</span>
</span></span><span class="line"><span class="cl"><span class="na">data-latest-firefox</span><span class="o">=</span><span class="s">&#34;113.0.1&#34;</span> <span class="na">data-esr-versions</span><span class="o">=</span><span class="s">&#34;102.11.0&#34;</span>
</span></span><span class="line"><span class="cl"><span class="na">data-gtm-container-id</span><span class="o">=</span><span class="s">&#34;GTM-MW3R8V&#34;</span> <span class="na">data-gtm-page-id</span><span class="o">=</span><span class="s">&#34;Homepage&#34;</span>
</span></span><span class="line"><span class="cl"><span class="na">data-stub-attribution-rate</span><span class="o">=</span><span class="s">&#34;1.0&#34;</span> <span class="na">data-convert-project-id</span><span class="o">=</span><span class="s">&#34;10039-1003343&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">...
</span></span></code></pre></div><p>If you want to learn more about how CORS proxies work, here&rsquo;s a <a href="https://httptoolkit.com/blog/cors-proxies" rel="noopener noreferrer" target="_blank">fantastic article on CORS
proxies</a> that explains the inner machinery in more detail.</p>
<h2 id="free-proxy-servers-can-be-pernicious">Free proxy servers can be pernicious</h2>
<p>Using a free CORS proxy server can be dangerous as it might spill the beans on your requests
and data to some random service you don&rsquo;t know or trust. Since it plays as a middleman
between your app and the resource you&rsquo;re after, it could potentially snoop on, mess with, or
keep tabs on your requests and data. Plus, some of those free CORS proxy servers might have
restrictions on the size, type, or number of requests they can handle, or they might not
even support HTTPS or other security bells and whistles.</p>
<h2 id="build-your-own-cors-proxy-with-cloudflare-workers">Build your own CORS proxy with Cloudflare Workers</h2>
<p>With all the intros out of the way, here&rsquo;s how CloudFlare Workers afforded me to prop up a
CORS proxy in less than half an hour. If you&rsquo;re impatient and just want to take a look at
the service in its full glory then head over to the <a href="https://github.com/rednafi/cors-proxy" rel="noopener noreferrer" target="_blank">cors-proxy repo</a>. GitHub Actions
deploys the service automatically to CloudFlare Workers every time a change is pushed to the
<code>main</code> branch.</p>
<h3 id="installing-the-prerequisites">Installing the prerequisites</h3>
<p>Assuming you have <code>node</code> installed on your system, you can fetch the <a href="https://developers.cloudflare.com/workers/wrangler/" rel="noopener noreferrer" target="_blank">wrangler CLI</a> with the
following command:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">npm install -g wrangler
</span></span></code></pre></div><p>This will allow us to develop and test the service locally.</p>
<h3 id="bootstrapping-the-service">Bootstrapping the service</h3>
<p>Create a new directory where you want to develop your service and bring it under source
control. Now, run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">npm create cloudflare@latest
</span></span></code></pre></div><p>The CLI will guide you through the entire bootstrapping process interactively. You&rsquo;ll have
to create a Cloudflare account (if you don&rsquo;t have one already) and log into the dashboard.
Then it&rsquo;ll prompt you to deploy your first <code>hello-world</code> API endpoint that you can
immediately start to play with without doing anything else. Being able to see the serverless
function in action within like 5 minutes gave me a huge dopamine boost that AWS Lambda never
could. You can see the interactive bootstrapping section here:</p>
<details>
<summary><strong>Complete CLI output...</strong></strong></summary>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">using create-cloudflare version 2.0.7
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">╭ Create an application with Cloudflare Step 1 of 3
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">├ Where do you want to create your application?
</span></span><span class="line"><span class="cl">│ dir cors-proxy
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">├ What type of application do you want to create?
</span></span><span class="line"><span class="cl">│ type &#34;Hello World&#34; script
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">├ Do you want to use TypeScript?
</span></span><span class="line"><span class="cl">│ typescript no
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">├ Copying files from &#34;simple&#34; template
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">╰ Application created
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">╭ Installing dependencies Step 2 of 3
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">├ Installing dependencies
</span></span><span class="line"><span class="cl">│ installed via `npm install`
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">╰ Dependencies Installed
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">╭ Deploy with Cloudflare Step 3 of 3
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">├ Do you want to deploy your application?
</span></span><span class="line"><span class="cl">│ yes deploying via `npm run deploy`
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">├ Logging into Cloudflare This will open a browser window
</span></span><span class="line"><span class="cl">│ allowed via `wrangler login`
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">├ Deploying your application
</span></span><span class="line"><span class="cl">│ deployed via `npm run deploy`
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">├  SUCCESS  View your deployed application at
</span></span><span class="line"><span class="cl">│  https://cors-proxy.rednafi.workers.dev (this may take a few mins)
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">│ Run the development server npm run dev
</span></span><span class="line"><span class="cl">│ Deploy your application npm run deploy
</span></span><span class="line"><span class="cl">│ Read the documentation https://developers.cloudflare.com/workers
</span></span><span class="line"><span class="cl">│ Stuck? Join us at https://discord.gg/cloudflaredev
</span></span><span class="line"><span class="cl">│
</span></span><span class="line"><span class="cl">╰ See you again soon!
</span></span></code></pre></div></details>
<p>Running the interactive session will create the following directory structure:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">├── src
</span></span><span class="line"><span class="cl">│   └── worker.js
</span></span><span class="line"><span class="cl">├── package-lock.json
</span></span><span class="line"><span class="cl">├── package.json
</span></span><span class="line"><span class="cl">└── wrangler.toml
</span></span></code></pre></div><h3 id="developing-the-cors-proxy">Developing the CORS proxy</h3>
<p>We&rsquo;ll write our proxy server in <code>src/worker.js</code> file. Copy the following JS snippet and
paste it to the file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">async</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">env</span><span class="p">,</span> <span class="nx">ctx</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Extract method, url and headers from the incoming request object.
</span></span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="p">{</span> <span class="nx">method</span><span class="p">,</span> <span class="nx">url</span><span class="p">,</span> <span class="nx">headers</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">request</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Extract destination url from the query string.
</span></span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">destUrl</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">URL</span><span class="p">(</span><span class="nx">url</span><span class="p">).</span><span class="nx">searchParams</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s2">&#34;url&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// If the destination url is not present, return 400.
</span></span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">destUrl</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="s2">&#34;Missing destination URL.&#34;</span><span class="p">,</span> <span class="p">{</span> <span class="nx">status</span><span class="o">:</span> <span class="mi">400</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// If the request method is OPTIONS, return CORS headers.
</span></span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="nx">method</span> <span class="o">===</span> <span class="s2">&#34;OPTIONS&#34;</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="cl">      <span class="nx">headers</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="s2">&#34;Origin&#34;</span><span class="p">)</span> <span class="o">&amp;&amp;</span>
</span></span><span class="line"><span class="cl">      <span class="nx">headers</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="s2">&#34;Access-Control-Request-Method&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">responseHeaders</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Access-Control-Allow-Origin&#34;</span><span class="o">:</span> <span class="nx">headers</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s2">&#34;Origin&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Access-Control-Allow-Methods&#34;</span><span class="o">:</span> <span class="s2">&#34;*&#34;</span><span class="p">,</span> <span class="c1">// Allow all methods
</span></span></span><span class="line"><span class="cl">        <span class="s2">&#34;Access-Control-Allow-Headers&#34;</span><span class="o">:</span> <span class="nx">headers</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">          <span class="s2">&#34;Access-Control-Request-Headers&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Access-Control-Max-Age&#34;</span><span class="o">:</span> <span class="s2">&#34;86400&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="p">};</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="p">{</span> <span class="nx">headers</span><span class="o">:</span> <span class="nx">responseHeaders</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">proxyRequest</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Request</span><span class="p">(</span><span class="nx">destUrl</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">method</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">headers</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="p">...</span><span class="nx">headers</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">Origin</span><span class="o">:</span> <span class="s2">&#34;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">proxyRequest</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">responseHeaders</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Headers</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">headers</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="nx">responseHeaders</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="s2">&#34;Access-Control-Allow-Origin&#34;</span><span class="p">,</span> <span class="s2">&#34;*&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="nx">responseHeaders</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="s2">&#34;Access-Control-Allow-Credentials&#34;</span><span class="p">,</span> <span class="s2">&#34;true&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="nx">responseHeaders</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="s2">&#34;Access-Control-Allow-Methods&#34;</span><span class="p">,</span> <span class="s2">&#34;*&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">status</span><span class="o">:</span> <span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">statusText</span><span class="o">:</span> <span class="nx">response</span><span class="p">.</span><span class="nx">statusText</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">headers</span><span class="o">:</span> <span class="nx">responseHeaders</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="s2">&#34;Error occurred while fetching the resource.&#34;</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">status</span><span class="o">:</span> <span class="mi">500</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="p">});</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><p>The first section of the code deals with extracting relevant information from the incoming
request. It destructures the <code>method</code>, <code>url</code>, and <code>headers</code> properties from the <code>request</code>
object, which represents the client&rsquo;s request.</p>
<p>Next, it extracts the destination URL from the query string. It extracts the URL parameter
using the <code>searchParams.get()</code> method. If the destination URL isn&rsquo;t provided, the function
returns a <code>Response</code> object with an error message and a status code of 400 (Bad Request).</p>
<p>The code then checks if the request method is <code>OPTIONS</code>. The <code>OPTIONS</code> method is used in
CORS <a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request" rel="noopener noreferrer" target="_blank">preflight requests</a> to determine if the actual request is safe to send. If the request
is an <code>OPTIONS</code> request and contains specific headers indicating a CORS preflight request
(<code>Origin</code> and <code>Access-Control-Request-Method</code>), the function generates a response with
appropriate CORS headers. The response headers include <code>Access-Control-Allow-Origin</code> to
reflect the client&rsquo;s origin, <code>Access-Control-Allow-Methods</code> set to <code>*</code>, allowing any HTTP
method, <code>Access-Control-Allow-Headers</code> based on the requested headers, and
<code>Access-Control-Max-Age</code> set to <code>86400</code> seconds (one day) to cache the preflight response.</p>
<p>If the request is not an <code>OPTIONS</code> request or doesn&rsquo;t meet the CORS preflight conditions,
the code continues execution. It creates a new <code>Request</code> object named <code>proxyRequest</code> uses
the extracted destination URL and sets the method and headers of the original request. The
<code>Origin</code> header is removed to prevent CORS restrictions when forwarding the request.</p>
<p>The subsequent code performs the actual request forwarding. It uses <code>fetch</code> to send the
<code>proxyRequest</code> to the destination URL. If the fetch is successful, the code proceeds to
process the response. It creates a new <code>Headers</code> object from the response&rsquo;s headers and
modifies them to include the necessary CORS headers.</p>
<p>Finally, the function constructs a <code>Response</code> object using the response <code>body</code>, <code>status</code>,
<code>statusText</code>, and modified <code>headers</code>. If an error occurs during the fetch operation, the
code catches the error and returns a <code>Response</code> object with an error message and a status
code of 500 (Internal Server Error).</p>
<p>Once you&rsquo;ve pasted the snippet, you can redeploy the service from your local machine with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">wrangler deploy
</span></span></code></pre></div><p>This will deploy the service immediately:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl"> ⛅️ wrangler 3.0.0
</span></span><span class="line"><span class="cl">------------------
</span></span><span class="line"><span class="cl">Total Upload: 1.52 KiB / gzip: 0.57 KiB
</span></span><span class="line"><span class="cl">Uploaded cors-proxy (0.51 sec)
</span></span><span class="line"><span class="cl">Published cors-proxy (0.38 sec)
</span></span><span class="line"><span class="cl">  https://&lt;your-deployed-service&gt;
</span></span><span class="line"><span class="cl">Current Deployment ID: f300ac99-c15e-4e30-a910-a56d81c10b95
</span></span></code></pre></div><p>I&rsquo;ve removed my root domain from the above output since I&rsquo;m using the free version of
Workers and don&rsquo;t want people to exhaust my free request quota. Haha, security by obscurity!
But once you&rsquo;ve deployed your proxy server, you can go to the following URL from your
browser:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">https://&lt;your-deployed-service&gt;/?url=https://mozilla.com
</span></span></code></pre></div><p>This will send you to the Mozilla website through the deployed function. Now you can use it
just like the free CORS proxy. Try it out by dropping the following snippet to your browser
console. This is exactly the same as the previous <code>fetch</code> snippet but the only difference is
this time, we&rsquo;re using our own proxy server that we control:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="c1">// Notice how we&#39;re prepending CORS URL before the target URL
</span></span></span><span class="line"><span class="cl"><span class="nx">fetch</span><span class="p">(</span><span class="s2">&#34;https://&lt;your-deployed-service&gt;?url=https://mozilla.org&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">response</span><span class="p">.</span><span class="nx">text</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Do something with the received data
</span></span></span><span class="line"><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Handle any errors that occurred during the request
</span></span></span><span class="line"><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="s2">&#34;Error:&#34;</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span></code></pre></div><p>Don&rsquo;t forget to replace the <code>&lt;your-deployed-service&gt;</code> URL with your own service. This will
result in a successful request. You can also interactively send requests to the destination
URLs via the Cloudflare Workers dashboard. Go to your Cloudflare dashboard, head over to the
Workers section, and select your deployed serverless function:</p>
<p><img alt="Cloudflare Workers dashboard showing CORS proxy code in web editor" loading="lazy" src="https://blob.rednafi.com/static/images/cors_proxy_with_cloudflare_workers/img_1.png"></p>
<h3 id="deploying-the-service-with-github-actions">Deploying the service with GitHub Actions</h3>
<p>For one-off services, <code>wrangler deploy</code> in the local machine works perfectly but I usually
don&rsquo;t consider a project fully done until I&rsquo;ve automated away the whole process. So, I wrote
a quick GitHub Actions workflow to run the linters and deploy the service automatically when
a new commit is pushed to the <code>main</code> branch. Here&rsquo;s how it looks:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="c"># .github/workers/ci.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Deploy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Allow running this workflow manually.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">workflow_dispatch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">build</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/setup-node@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">node-version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;lts/*&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cache</span><span class="p">:</span><span class="w"> </span><span class="l">npm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cache-dependency-path</span><span class="p">:</span><span class="w"> </span><span class="l">cors-proxy/package-lock.json</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Install dependencies</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">working-directory</span><span class="p">:</span><span class="w"> </span><span class="l">./cors-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          npm install</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Run linter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">working-directory</span><span class="p">:</span><span class="w"> </span><span class="l">./cors-proxy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          npx prettier --check .</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">build</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Publish</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">cloudflare/wrangler-action@2.0.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">apiToken</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.CLOUDFLARE_API_TOKEN }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">workingDirectory</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;cors-proxy&#34;</span><span class="w">
</span></span></span></code></pre></div><p>For this to work, you&rsquo;ll need to create a <a href="https://developers.cloudflare.com/fundamentals/api/get-started/create-token/" rel="noopener noreferrer" target="_blank">Cloudflare API token</a> and add it to the <a href="https://docs.github.com/en/actions/security-guides/encrypted-secrets" rel="noopener noreferrer" target="_blank">GitHub
Secrets</a> of your proxy server&rsquo;s repository. Here&rsquo;s the <a href="https://github.com/rednafi/cors-proxy/blob/main/.github/workflows/ci.yml" rel="noopener noreferrer" target="_blank">complete CI workflow file</a>.</p>
<!-- references -->
<!-- prettier-ignore-start -->
<!-- prettier-ignore-end -->
]]></content:encoded>
    </item>
    <item>
      <title>Periodic readme updates with GitHub Actions</title>
      <link>https://rednafi.com/javascript/periodic-readme-updates-with-gh-actions/</link>
      <pubDate>Thu, 04 May 2023 00:00:00 +0000</pubDate>
      <guid>https://rednafi.com/javascript/periodic-readme-updates-with-gh-actions/</guid>
      <description>Automate your GitHub profile README with periodic updates using Actions. Parse RSS feeds and dynamically display latest blog posts with NodeJS.</description>
      <category>JavaScript</category>
      <category>GitHub</category>
      <category>DevOps</category>
      <content:encoded><![CDATA[<p>I recently gave my <a href="https://rednafi.com/">personal blog</a> a fresh new look and decided it was time to spruce up my
<a href="https://github.com/rednafi/" rel="noopener noreferrer" target="_blank">GitHub profile</a>&rsquo;s landing page as well. GitHub has a <a href="https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-profile/customizing-your-profile/managing-your-profile-readme" rel="noopener noreferrer" target="_blank">special way of treating the README.md
file</a> of your <your-username> repo, displaying its content as the landing page for your
profile. My goal was to showcase a brief introduction about myself and my work, along with a
list of the five most recent articles on my blog. Additionally, I wanted to ensure that the
article list stayed up to date.</p>
<p>There are plenty of fancy GitHub Action workflows like the <a href="https://github.com/gautamkrishnar/blog-post-workflow" rel="noopener noreferrer" target="_blank">blog-post-workflow</a> that allow
you to add your site&rsquo;s URL to the CI file and it&rsquo;ll periodically fetch the most recent
content from the source and update the readme file. However, I wanted to make a simpler
version of it from scratch which can be extended for periodically updating any Markdown file
in any repo, just not the profile readme. So, here&rsquo;s the plan:</p>
<ul>
<li>A custom GitHub Action workflow will periodically run a nodejs script.</li>
<li>The script will then:
<ul>
<li>Grab the <a href="https://rednafi.com/index.xml">XML index</a> of this blog that you&rsquo;re reading.</li>
<li>Parse the XML content and extract the URLs and publication dates of 5 most recent
articles.</li>
<li>Update the associated Markdown table with the extracted content on the profile&rsquo;s
<code>README.md</code> file.</li>
</ul>
</li>
<li>Finally, the workflow will commit the changes and push them to the profile repo. You can
see the final outcome in the <a href="https://github.com/rednafi/rednafi" rel="noopener noreferrer" target="_blank">profile repository</a>.</li>
</ul>
<p>Here&rsquo;s the script that performs the above steps:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="c1">// importBlogs.js
</span></span></span><span class="line"><span class="cl"><span class="cm">/* Import the latest 5 blog posts from rss feed */</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">fetch</span> <span class="nx">from</span> <span class="s2">&#34;node-fetch&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">Parser</span> <span class="p">}</span> <span class="nx">from</span> <span class="s2">&#34;xml2js&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">promises</span> <span class="p">}</span> <span class="nx">from</span> <span class="s2">&#34;fs&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">rssUrl</span> <span class="o">=</span> <span class="s2">&#34;https://rednafi.com/index.xml&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">header</span> <span class="o">=</span> <span class="sb">`&lt;div align=&#34;center&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="sb">    Introducing myself...
</span></span></span><span class="line"><span class="cl"><span class="sb">&lt;div&gt;\n\n`</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">outputFile</span> <span class="o">=</span> <span class="s2">&#34;README.md&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">parser</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Parser</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Define an async function to get and parse the rss data
</span></span></span><span class="line"><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">getRssData</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">rssUrl</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="kr">await</span> <span class="nx">res</span><span class="p">.</span><span class="nx">text</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kr">await</span> <span class="nx">parser</span><span class="p">.</span><span class="nx">parseStringPromise</span><span class="p">(</span><span class="nx">data</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Define an async function to write the output file
</span></span></span><span class="line"><span class="cl"><span class="kr">async</span> <span class="kd">function</span> <span class="nx">writeOutputFile</span><span class="p">(</span><span class="nx">output</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">await</span> <span class="nx">promises</span><span class="p">.</span><span class="nx">writeFile</span><span class="p">(</span><span class="nx">outputFile</span><span class="p">,</span> <span class="nx">output</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Saved </span><span class="si">${</span><span class="nx">outputFile</span><span class="si">}</span><span class="sb">`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Call the async functions
</span></span></span><span class="line"><span class="cl"><span class="nx">getRssData</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="nx">then</span><span class="p">((</span><span class="nx">result</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Get the first five posts from the result object
</span></span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">posts</span> <span class="o">=</span> <span class="nx">result</span><span class="p">.</span><span class="nx">rss</span><span class="p">.</span><span class="nx">channel</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">item</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">5</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Initialize an empty output string
</span></span></span><span class="line"><span class="cl">    <span class="kd">let</span> <span class="nx">output</span> <span class="o">=</span> <span class="s2">&#34;&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Add a title to the output string
</span></span></span><span class="line"><span class="cl">    <span class="nx">output</span> <span class="o">+=</span> <span class="nx">header</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Add a header row to the output string
</span></span></span><span class="line"><span class="cl">    <span class="nx">output</span> <span class="o">+=</span> <span class="sb">`#### Recent articles\n\n`</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">output</span> <span class="o">+=</span> <span class="s2">&#34;| Title | Published On |\n&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">output</span> <span class="o">+=</span> <span class="s2">&#34;| ----- | ------------ |\n&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Loop through posts and add a row for each to output
</span></span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">post</span> <span class="k">of</span> <span class="nx">posts</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="c1">// Strip the time from the pubDate
</span></span></span><span class="line"><span class="cl">      <span class="kr">const</span> <span class="nx">date</span> <span class="o">=</span> <span class="nx">post</span><span class="p">.</span><span class="nx">pubDate</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">16</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">      <span class="nx">output</span> <span class="o">+=</span> <span class="sb">`| [</span><span class="si">${</span><span class="nx">post</span><span class="p">.</span><span class="nx">title</span><span class="si">}</span><span class="sb">](</span><span class="si">${</span><span class="nx">post</span><span class="p">.</span><span class="nx">link</span><span class="si">}</span><span class="sb">) | </span><span class="si">${</span><span class="nx">date</span><span class="si">}</span><span class="sb"> |\n`</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Call the writeOutputFile function with the output string
</span></span></span><span class="line"><span class="cl">    <span class="nx">writeOutputFile</span><span class="p">(</span><span class="nx">output</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">})</span>
</span></span><span class="line"><span class="cl">  <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">err</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Handle the error
</span></span></span><span class="line"><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span></code></pre></div><p>The snippet above utilizes <code>node-fetch</code> to make HTTP calls,<code>xml2js</code> for XML parsing, and the
built-in <code>fs</code> module&rsquo;s <code>promises</code> for handling file system operations.</p>
<p>Next, it defines an async function <code>getRssData</code> responsible for fetching the XML data from
the [https://rednafi.com/index.xml] URL. It extracts the blog URLs and publication dates,
and returns the parsed data as a list of objects. Another async function, <code>writeOutputFile</code>,
writes the parsed XML content as a Markdown table and saves it to the <code>README.md</code> file.</p>
<p>The script is executed by the following GitHub Action workflow every day at 0:00 UTC. Before
the CI runs, make sure you create a new <a href="https://docs.github.com/en/rest/actions/secrets?apiVersion=2022-11-28" rel="noopener noreferrer" target="_blank">Action Secret</a> that houses a <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token" rel="noopener noreferrer" target="_blank">personal access
token</a> with write access to the repo where the CI runs.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="c"># Run a bash script to randomly generate empty commit to this repo.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">CI</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Don&#39;t run on push; this CI pushes (would cause infinite loop)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># push: [ main ]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Add a schedule to run the job every day at 0:00 UTC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schedule</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">cron</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0 0 * * *&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Allow running this workflow manually</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">workflow_dispatch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">build</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Checkout repo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="c"># Required for pushing refs to destination repository</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">fetch-depth</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ref</span><span class="p">:</span><span class="w"> </span><span class="l">${{ github.head_ref }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">token</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.ACCESS_TOKEN }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/setup-node@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">node-version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;lts/*&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cache</span><span class="p">:</span><span class="w"> </span><span class="l">npm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cache-dependency-path</span><span class="p">:</span><span class="w"> </span><span class="l">package-lock.json</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Install dependencies</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          npm install</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Run linter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          npx prettier --write .</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Run script</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          node scripts/importBlogs.js</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Commit changes</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          git config --local user.name \
</span></span></span><span class="line"><span class="cl"><span class="sd">            &#34;github-actions[bot]&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">          git config --local user.email \
</span></span></span><span class="line"><span class="cl"><span class="sd">            &#34;41898282+github-actions[bot]@users.noreply.github.com&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">          git add .
</span></span></span><span class="line"><span class="cl"><span class="sd">          git diff-index --quiet HEAD \
</span></span></span><span class="line"><span class="cl"><span class="sd">            || git commit -m &#34;Autocommit: updated at $(date -u)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Push changes</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">ad-m/github-push-action@master</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">force_with_lease</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><p>In the first four steps, the workflow checks out the codebase, sets up nodejs, installs the
dependencies, and then runs <code>prettier</code> on the scripts. Next, it executes the
<code>importBlogs.js</code> script. The script updates the README and the subsequent shell commands
commit the changes to the repo. The following line ensures that we&rsquo;re only trying to commit
when there&rsquo;s a change in the tracked files.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git diff-index --quiet HEAD <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="o">||</span> git commit -m <span class="s2">&#34;Autocommit: updated at </span><span class="k">$(</span>date -u<span class="k">)</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>Then in the last step, we use an off-the-shelf workflow to push our changes to the repo.
Check out the <a href="https://github.com/rednafi/rednafi/tree/master/.github/workflows" rel="noopener noreferrer" target="_blank">workflow directory</a> of my profile&rsquo;s repo to see the whole setup in action.
I&rsquo;m quite satisfied with the final output:</p>
<p><img alt="GitHub profile README showing auto-updated table of latest blog posts" loading="lazy" src="https://blob.rednafi.com/static/images/periodic_readme_updates_with_gh_actions/img_1.png"></p>
<!-- references -->
<!-- prettier-ignore-start -->
<!-- prettier-ignore-end -->
]]></content:encoded>
    </item>
    <item>
      <title>Auditing commit messages on GitHub</title>
      <link>https://rednafi.com/misc/audit-commit-messages-on-github/</link>
      <pubDate>Thu, 06 Oct 2022 00:00:00 +0000</pubDate>
      <guid>https://rednafi.com/misc/audit-commit-messages-on-github/</guid>
      <description>Automate commit message validation with GitHub Actions. Enforce refs and closes patterns to maintain clean Git history and link commits to issues.</description>
      <category>GitHub</category>
      <category>DevOps</category>
      <category>Shell</category>
      <content:encoded><![CDATA[<p>After reading Simon Willison&rsquo;s <a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/" rel="noopener noreferrer" target="_blank">amazing piece on how he builds a feature</a>, I wanted to adopt
some of the good practices and incorporate them into my own workflow. One of the highlights
of that post was how to kick off a feature work. The process roughly goes like this:</p>
<ul>
<li>Opening a new GitHub issue for the feature in the corresponding repository.</li>
<li>Adding a rough description of the feature to the issue.</li>
<li>Creating a feature branch off of <code>main/master/trunk</code>. If the feature is trivial or just a
doc update, this step can be skipped.</li>
<li>Referring to the issue in every commit message as you start working on the feature:
<ul>
<li>Appending <code>#refs &lt;issue-number&gt;</code> to every commit message. This will attach the commit
to the concerning issue on the GitHub UI.</li>
<li>Appending <code>#closes &lt;issue-number&gt;</code> to the final commit message when the feature is
complete.</li>
<li>If you need to refer to an issue after it&rsquo;s closed, you can still do that by appending
<code>#refs &lt;issue-number&gt;</code> to the commit message. So a commit message should look similar
to <code>Feature foo, refs #120</code> or <code>Update foo, closes #115</code>. The comma (<code>,</code>) before
<code>refs/closes</code> is essential here. I like to enforce it.</li>
</ul>
</li>
</ul>
<p>This pattern also works for bugfixes without any changes. Here&rsquo;s an <a href="https://github.com/rednafi/reflections/issues/170" rel="noopener noreferrer" target="_blank">example issue</a> that
shows the workflow in action. Plus, I follow a similar pattern to write the blogs on this
site as well. This is what a feature issue might look like on GitHub:</p>
<p><img alt="GitHub issue showing commits linked with refs and closes keywords" loading="lazy" src="https://blob.rednafi.com/static/images/audit_commit_messages_on_github/img_1.png"></p>
<p>While I&rsquo;m quite happy with how the process is working for me, often time, I get careless and
push commits without a reference to any issue. This pollutes the Git history and breaks my
streak of maintaining good hygiene. So, I was looking for a way to make sure that the CI
fails and reprimands me whenever I&rsquo;m not following the process correctly. It&rsquo;s just one less
thing to worry about.</p>
<p>I&rsquo;ve decided to use GitHub Actions to audit the conformity of the commit messages. The CI
pipeline is orchestrated as follows:</p>
<ul>
<li>After every push and pull-request, the <code>audit-commits</code> job in an <code>audit.yml</code> workflow file
will verify the conformity of the commit messages. This job runs a regex pattern against
every commit message and fails with exit code 1 if the message doesn&rsquo;t respect the
expected format.</li>
<li>If the <code>audit-commits</code> job passes successfully, only then the primary jobs in the <code>ci.yml</code>
workflow will execute. The entire pipeline will fail and the primary CI workflow won&rsquo;t be
triggered at all if the <code>audit-commit</code> job fails at any point.</li>
</ul>
<p>On GitHub, you&rsquo;re expected to place your workflow files in the <code>.github/workflows</code>
directory. If you inspect this blog&rsquo;s <a href="https://github.com/rednafi/reflections/tree/master/.github/workflows" rel="noopener noreferrer" target="_blank">workflows folder</a>, you&rsquo;ll see this pattern in action.
Here, the directory has three workflow files:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">.github/workflows
</span></span><span class="line"><span class="cl">├── audit.yml
</span></span><span class="line"><span class="cl">├── automerge.yml
</span></span><span class="line"><span class="cl">└── ci.yml
</span></span></code></pre></div><p>The <code>automerge.yml</code> file automatically merges a pull-request when the primary CI jobs pass.
I wrote about it in more detail in <a href="/misc/automerge-dependabot-prs-on-github/">another post about automerging Dependabot PRs</a>. We&rsquo;ll
ignore the <code>automerge.yml</code> file for now. Here, the audit file runs after every push and
pull-request and verifies the structure of the commit message. I picked a generic name like
<code>audit.yml</code> instead of a more specific one like <code>audit-commit.yml</code> because in the future if
I want to add another check, I can easily extend this file without renaming it. Here&rsquo;s the
unabridged content of the <code>audit.yml</code> file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="c"># .github/workflows/audit.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Auditing commit structure.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Audit</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">workflow_call</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">audit-commits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">${{ github.actor != &#39;dependabot[bot]&#39; }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Check commit message format&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">shell</span><span class="p">:</span><span class="w"> </span><span class="l">bash</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          set -euo pipefail
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          # Get the commit payload from GH Actions push event.
</span></span></span><span class="line"><span class="cl"><span class="sd">          # See GitHub&#39;s webhook-events docs for details
</span></span></span><span class="line"><span class="cl"><span class="sd">          commits=&#39;${{ toJSON(github.event.commits) }}&#39;
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          # Exit with 0 if no new commit is found.
</span></span></span><span class="line"><span class="cl"><span class="sd">          if [[ $commits =~ &#34;null&#34; ]]; then
</span></span></span><span class="line"><span class="cl"><span class="sd">              echo &#34;No commit found. Exiting...&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">          exit 0
</span></span></span><span class="line"><span class="cl"><span class="sd">          fi
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          # Get the unique messages from the commits event.
</span></span></span><span class="line"><span class="cl"><span class="sd">          parsed=$(echo -n &#34;$commits&#34; | jq -r &#34;.[].message&#34; | sort -u)
</span></span></span><span class="line"><span class="cl"><span class="sd">          mtch=&#39;(, refs|, closes) #[0-9]+&#39;
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          echo &#34;$parsed&#34; | while IFS= read -r raw_line; do
</span></span></span><span class="line"><span class="cl"><span class="sd">              line=$(echo &#34;$raw_line&#34; | tr -d &#34;\r\n&#34;)
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">              # Ignore empty lines.
</span></span></span><span class="line"><span class="cl"><span class="sd">              if [[ -z &#34;$line&#34; ]]; then
</span></span></span><span class="line"><span class="cl"><span class="sd">                  continue
</span></span></span><span class="line"><span class="cl"><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">              # Check for &#39;refs #N&#39; or &#39;closes #N&#39;. Else error.
</span></span></span><span class="line"><span class="cl"><span class="sd">              elif [[ &#34;$line&#34; =~ $mtch ]]; then
</span></span></span><span class="line"><span class="cl"><span class="sd">                  echo &#34;Commit message: $line ✅&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">              else
</span></span></span><span class="line"><span class="cl"><span class="sd">                  echo &#34;Commit message: $line ❌&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">                  echo -n &#34;Commit message must contain &#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">                  echo -n &#34;&#39;refs #issue_number&#39; or &#39;closes #issue_number&#39;.&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">                  exit 1
</span></span></span><span class="line"><span class="cl"><span class="sd">              fi
</span></span></span><span class="line"><span class="cl"><span class="sd">          done</span><span class="w">
</span></span></span></code></pre></div><p>I&rsquo;ve defined this workflow as a reusable one. A reusable workflow can be called like a
function with parameters from another workflow. The <code>workflow_call</code> node the <code>audit.yml</code>
file makes it a reusable one and you can define additional parameters in this section if you
need to do so. However, in this particular case, I don&rsquo;t need to pass any parameters while
calling the <code>audit.yml</code> workflow from the <code>ci.yml</code> workflow. You can find more details on
how to define <a href="https://docs.github.com/en/actions/using-workflows/reusing-workflows" rel="noopener noreferrer" target="_blank">reusable workflows</a> in the docs.</p>
<p>In the <code>jobs</code> section of the <code>audit.yml</code> file, we define a single <code>audit-commits</code> job that
runs a bash script against every incoming commit message and verifies its structure. The
commit messages can be accessed from the <code>'${{ toJSON(github.event.commits) }}'</code> context
variable. Then the script loops over every commit message and verifies the structure. It&rsquo;ll
terminate the job with exit code <code>1</code> if the incoming message doesn&rsquo;t match the expected
structure. Otherwise, the script will gracefully terminate the job with exit code <code>0</code>.</p>
<p>In the main <code>ci.yml</code> file the <code>audit.yml</code> workflow is called like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="nn">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">audit</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">rednafi/reflections/.github/workflows/audit.yml@master</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">...</span><span class="w">
</span></span></span></code></pre></div><p>The <code>ci.yml</code> file roughly looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">CI</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pull_request</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Everyday at 0:37 UTC.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schedule</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">cron</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;37 0 * * *&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Cancel any running workflow if the CI gets triggered again.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">concurrency</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">${{ github.head_ref || github.run_id }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cancel-in-progress</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">audit</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">rednafi/reflections/.github/workflows/audit.yml@master</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">build</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;audit&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="l">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;build&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="l">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;deploy&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="l">...</span><span class="w">
</span></span></span></code></pre></div><p>Here the <code>needs: [&quot;audit&quot;]</code> node in the <code>build</code> section ensures that the build will only
trigger if the <code>audit</code> job passes successfully. Otherwise, none of the <code>build</code>, <code>test</code>, or
<code>deploy</code> jobs will run and the CI will fail with a non-zero exit code. Here&rsquo;s the <a href="https://github.com/rednafi/reflections/blob/master/.github/workflows/ci.yml" rel="noopener noreferrer" target="_blank">fully
working ci.yml file</a>.</p>
<h2 id="notes">Notes</h2>
<p>GitHub Actions terminology can be confusing.</p>
<ul>
<li>A <strong>workflow</strong> is a separate file that contains one or more <strong>jobs</strong>.</li>
<li>A <strong>job</strong> is a set of steps in a workflow that executes on the same <strong>runner</strong>.</li>
<li>A <strong>runner</strong> is a server that runs your workflows when they&rsquo;re triggered. Each runner can
run a single job at a time.</li>
<li>A <strong>reusable</strong> workflow can be called from another workflow file.</li>
</ul>
<p>The docs have more information on <a href="https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions" rel="noopener noreferrer" target="_blank">understanding GitHub Actions</a>.</p>
<!-- references -->
<!-- prettier-ignore-start -->
<!-- prettier-ignore-end -->
]]></content:encoded>
    </item>
    <item>
      <title>When to use &#39;git pull --rebase&#39;</title>
      <link>https://rednafi.com/misc/when-to-use-git-pull-rebase/</link>
      <pubDate>Thu, 14 Jul 2022 00:00:00 +0000</pubDate>
      <guid>https://rednafi.com/misc/when-to-use-git-pull-rebase/</guid>
      <description>Fix divergent branch errors with git pull --rebase. Learn when to rebase local commits on top of remote changes for cleaner Git history.</description>
      <category>Git</category>
      <category>TIL</category>
      <category>GitHub</category>
      <content:encoded><![CDATA[<p>Whenever your local branch diverges from the remote branch, you can&rsquo;t directly pull from the
remote branch and merge it into the local branch. This can happen when, for example:</p>
<ul>
<li>You checkout from the <code>main</code> branch to work on a feature in a branch named <code>alice</code>.</li>
<li>When you&rsquo;re done, you merge <code>alice</code> into <code>main</code>.</li>
<li>After that, if you try to pull the <code>main</code> branch from remote again and the content of the
<code>main</code> branch changes by this time, you&rsquo;ll encounter a merge error.</li>
</ul>
<h2 id="reproduce-the-issue">Reproduce the issue</h2>
<p>Create a new branch named <code>alice</code> from <code>main</code>. Run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git checkout -b alice
</span></span></code></pre></div><p>From <code>alice</code> branch, add a line to a newly created file <code>foo.txt</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;from branch alice&#34;</span> &gt;&gt; foo.txt
</span></span></code></pre></div><p>Add, commit, and push the branch:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git commit -am <span class="s2">&#34;From branch alice&#34;</span> <span class="o">&amp;&amp;</span> git push
</span></span></code></pre></div><p>From the GitHub UI, send a pull request against the <code>main</code> branch and merge it:</p>
<p><img alt="GitHub pull request UI showing merge from alice branch to main" loading="lazy" src="https://blob.rednafi.com/static/images/when_to_use_git_pull_rebase/img_1.png"></p>
<p>In your local machine, switch to <code>main</code> and try to pull the latest content merged from the
<code>alice</code> branch. You&rsquo;ll encounter the following error:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-txt" data-lang="txt"><span class="line"><span class="cl">hint: You have divergent branches and need to specify how to reconcile them.
</span></span><span class="line"><span class="cl">hint: You can do so by running one of the following commands sometime before
</span></span><span class="line"><span class="cl">hint: your next pull:
</span></span><span class="line"><span class="cl">hint:
</span></span><span class="line"><span class="cl">hint:   git config pull.rebase false  # merge (the default strategy)
</span></span><span class="line"><span class="cl">hint:   git config pull.rebase true   # rebase
</span></span><span class="line"><span class="cl">hint:   git config pull.ff only       # fast-forward only
</span></span><span class="line"><span class="cl">hint:
</span></span><span class="line"><span class="cl">hint: You can replace &#34;git config&#34; with &#34;git config --global&#34; to set a default
</span></span><span class="line"><span class="cl">hint: preference for all repositories. You can also pass --rebase, --no-rebase,
</span></span><span class="line"><span class="cl">hint: or --ff-only on the command line to override the configured default per
</span></span><span class="line"><span class="cl">hint: invocation.
</span></span><span class="line"><span class="cl">fatal: Need to specify how to reconcile divergent branches.
</span></span></code></pre></div><p>This means that the history of your local <code>main</code> branch and the remote <code>main</code> branch have
diverged and they aren&rsquo;t reconciliable.</p>
<h2 id="solution">Solution</h2>
<p>From the <code>main</code> branch, you can run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">git pull --rebase
</span></span></code></pre></div><p>This will rebase your local <code>main</code> by adding your local commits on top of the remote
commits.</p>
<h2 id="further-reading">Further reading</h2>
<ul>
<li><a href="https://stackoverflow.com/questions/2472254/when-should-i-use-git-pull-rebase" rel="noopener noreferrer" target="_blank">When should I use git pull &ndash;rebase</a></li>
<li><a href="https://github.com/rednafi/_pull-rebase" rel="noopener noreferrer" target="_blank">An example repo that reproduces the issue</a></li>
</ul>
<!-- references -->
<!-- prettier-ignore-start -->
<!-- prettier-ignore-end -->
]]></content:encoded>
    </item>
    <item>
      <title>Automerge Dependabot PRs on GitHub</title>
      <link>https://rednafi.com/misc/automerge-dependabot-prs-on-github/</link>
      <pubDate>Thu, 07 Jul 2022 00:00:00 +0000</pubDate>
      <guid>https://rednafi.com/misc/automerge-dependabot-prs-on-github/</guid>
      <description>Automatically merge Dependabot pull requests using GitHub Actions. Configure branch protection and status checks for safe automated dependency updates.</description>
      <category>GitHub</category>
      <category>DevOps</category>
      <category>CLI</category>
      <content:encoded><![CDATA[<p>Whether I&rsquo;m trying out a new tool or just prototyping with a familiar stack, I usually
create a new project on GitHub and run all the experiments there. Some examples of these
are:</p>
<ul>
<li><a href="https://github.com/rednafi/rubric" rel="noopener noreferrer" target="_blank">rubric</a>: linter config initializer for Python</li>
<li><a href="https://github.com/rednafi/exert" rel="noopener noreferrer" target="_blank">exert</a>: declaratively apply converter functions to class attributes</li>
<li><a href="https://github.com/rednafi/hook-slinger" rel="noopener noreferrer" target="_blank">hook-slinger</a>: generic service to send, retry, and manage webhooks</li>
<li><a href="https://github.com/rednafi/think-async" rel="noopener noreferrer" target="_blank">think-async</a>: exploring cooperative concurrency primitives in Python</li>
<li><a href="https://github.com/rednafi/epilog" rel="noopener noreferrer" target="_blank">epilog</a>: container log aggregation with Elasticsearch, Kibana &amp; Filebeat</li>
</ul>
<p>While many of these prototypes become full-fledged projects, most end up being just one-time
journies. One common theme among all of these endeavors is that I always include
instructions in the <code>readme.md</code> on how to get the project up and running - no matter how
small it is. Also, I tend to configure a rudimentary CI pipeline that runs the linters and
tests. GitHub Actions and <a href="https://docs.github.com/en/code-security/dependabot/dependabot-security-updates/configuring-dependabot-security-updates" rel="noopener noreferrer" target="_blank">Dependabot</a> make it simple to configure a basic CI workflow.
Dependabot keeps the dependencies fresh and makes pull requests automatically when there&rsquo;s a
new version of a dependency used in a project.</p>
<p>Things can get quickly out of hand if you&rsquo;ve got a large collection of repos where the
automated CI runs periodically. Every now and then, I get a sizable volume of PRs in these
fairly stale repos that I still want to keep updated. Merging these manually is a chore.
Luckily, there are <a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/automatically-merging-a-pull-request" rel="noopener noreferrer" target="_blank">multiple ways to automatically merge PRs</a> that GitHub offers. The
workflow that is documented here is the one I happen to like the most. I also think that
this process leads to the path of the least surprise. Instead of depending on a bunch of
GitHub settings, we&rsquo;ll write a <a href="https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request" rel="noopener noreferrer" target="_blank">GitHub Actions workflow to enable auto-merge</a> to automate
the process.</p>
<p>First, you&rsquo;ll need to turn on the auto-merge option from the repository settings. To do so,
go to the repo&rsquo;s <em>settings</em> tab and turn on the <em>Allow auto-merge</em> option from the <em>Pull
Requests</em> section:</p>
<p><img alt="GitHub repository settings showing Allow auto-merge checkbox in Pull Requests section" loading="lazy" src="https://blob.rednafi.com/static/images/automerge_dependabot_prs_on_github/img_1.png"></p>
<p>Now, you probably don&rsquo;t want to mindlessly merge every pull request Dependabot throws at
you. You most likely want to make sure that a pull request triggers certain tests and it&rsquo;ll
be merged only if all of those checks pass. To do so, you can turn on <a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches" rel="noopener noreferrer" target="_blank">branch protection</a>.
From the <em>settings</em> panel, select <em>Branches</em> on the left panel:</p>
<p><img alt="GitHub settings Branches panel for adding branch protection rules" loading="lazy" src="https://blob.rednafi.com/static/images/automerge_dependabot_prs_on_github/img_2.png"></p>
<p>Once you&rsquo;ve selected the tab, add a branch protection rule to the target branch against
which Dependabot will send the pull requests:</p>
<p><img alt="GitHub branch protection rule configuration with status checks enabled" loading="lazy" src="https://blob.rednafi.com/static/images/automerge_dependabot_prs_on_github/img_3.png"></p>
<p>In this case, I&rsquo;m adding the protection layer to the <code>main</code> branch. I&rsquo;ve turned on the
<em>Require status checks to pass before merging</em> toggle and added the <code>build</code> step to the list
of status checks that are required. Here, you can select any job from your CI files in the
<code>.github/workflows</code> directory:</p>
<p><img alt="GitHub status checks dropdown showing build job selected as required check" loading="lazy" src="https://blob.rednafi.com/static/images/automerge_dependabot_prs_on_github/img_4.png"></p>
<p>Once this is done, you can drop the following CI file in the <code>.github/workflows</code> directory
of your repo. It&rsquo;s the same <a href="https://github.com/rednafi/reflections/blob/master/.github/workflows/automerge.yml" rel="noopener noreferrer" target="_blank">automerge workflow file</a> that&rsquo;s currently living inside this
site&rsquo;s CI folder.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="c"># .github/workflows/automerge.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Dependabot auto-merge</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w"> </span><span class="l">pull_request</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pull-requests</span><span class="p">:</span><span class="w"> </span><span class="l">write </span><span class="w"> </span><span class="c"># Needed if in a private repository</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">dependabot</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l">${{ github.actor == &#39;dependabot[bot]&#39; }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Enable auto-merge for Dependabot PRs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">gh pr merge --auto --merge &#34;$PR_URL&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">PR_URL</span><span class="p">:</span><span class="w"> </span><span class="l">${{github.event.pull_request.html_url}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="c"># GitHub provides this variable in the CI env. You don&#39;t</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="c"># need to add anything to the secrets vault.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">GITHUB_TOKEN</span><span class="p">:</span><span class="w"> </span><span class="l">${{ secrets.GITHUB_TOKEN }}</span><span class="w">
</span></span></span></code></pre></div><p>From now on, every time Dependabot sends a merge request, the checks will be triggered and
if all the mandatory checks pass, the <code>automerge.yml</code> workflow will merge it into the target
branch.</p>
<!-- references -->
<!-- prettier-ignore-start -->
<!-- prettier-ignore-end -->
]]></content:encoded>
    </item>
    <item>
      <title>Github action template for Python based projects</title>
      <link>https://rednafi.com/python/github-action-template-python/</link>
      <pubDate>Wed, 02 Mar 2022 00:00:00 +0000</pubDate>
      <guid>https://rednafi.com/python/github-action-template-python/</guid>
      <description>Production-ready GitHub Actions workflow for Python with multi-OS testing, dependency caching, automated updates, and daily scheduled runs.</description>
      <category>Python</category>
      <category>GitHub</category>
      <category>DevOps</category>
      <content:encoded><![CDATA[<p>Five traits that almost all the GitHub Action workflows in my Python projects share are:</p>
<ul>
<li>If a new workflow is triggered while the previous one is running, the first one will get
canceled.</li>
<li>The CI is triggered every day at UTC 1.</li>
<li>Tests and the lint-checkers are run on Ubuntu and MacOS against multiple Python versions.</li>
<li>Pip dependencies are cached.</li>
<li>Dependencies, including the Actions dependencies are automatically updated via
<a href="https://github.com/dependabot" rel="noopener noreferrer" target="_blank">Dependabot</a>.</li>
</ul>
<p>I use <a href="https://github.com/jazzband/pip-tools" rel="noopener noreferrer" target="_blank">pip-tools</a> for managing dependencies in applications and <a href="https://github.com/pypa/setuptools" rel="noopener noreferrer" target="_blank">setuptools</a> <code>setup.py</code>
combo for managing dependencies in libraries. Here&rsquo;s an annotated version of the template
action syntax:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="c"># .github/workflows/ci.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">CI</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Triggers when something is pushed to the &#39;main&#39; branch.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">master</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Triggers when a pull request is sent against the &#39;main&#39; branch.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pull_request</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">master</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Triggers everyday at 1 UTC.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schedule</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">cron</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0 1 * * *&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Cancel any running workflow if the CI gets triggered again.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">concurrency</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">${{ github.head_ref || github.run_id }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cancel-in-progress</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">run-tests</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Tests are run on multiple Python versions.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">${{ matrix.os }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">matrix</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># Multiple OSs.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">ubuntu-latest, macos-latest]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># Multiple Python versions.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">python-version</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;3.10&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;3.11&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;3.12&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">include</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">~/.cache/pip</span><span class="w"> </span><span class="c"># Cache location on Ubuntu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">macos-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">~/Library/Caches/pip</span><span class="w"> </span><span class="c"># Cache location on MacOS</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Checkout to the codebase.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Sets up Python.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/setup-python@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">python-version</span><span class="p">:</span><span class="w"> </span><span class="l">${{ matrix.python-version }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Cache pip dependencies via &#39;cache&#39; actions.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/cache@v2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">${{ matrix.path }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;${{ runner.os }}-pip-${{ hashFiles(&#39;requirements*.txt&#39;) }}&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">restore-keys</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">            ${{ runner.os }}-pip-</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Dev and app dependencies are kept in separate files.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Install the Dependencies</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          pip install --upgrade pip
</span></span></span><span class="line"><span class="cl"><span class="sd">          pip install -r requirements.txt
</span></span></span><span class="line"><span class="cl"><span class="sd">          pip install -r requirements-dev.txt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Run black, isort, flake8, etc.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Check Linter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          echo &#34;Checking black formatting...&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">          python3 -m black --check .
</span></span></span><span class="line"><span class="cl"><span class="sd">          echo &#34;Checking isort formatting...&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">          python3 -m isort --check .
</span></span></span><span class="line"><span class="cl"><span class="sd">          echo &#34;Checking flake8 formatting...&#34;
</span></span></span><span class="line"><span class="cl"><span class="sd">          python3 -m flake8 .</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Run the tests via pytest.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Run the tests</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">          pytest -v -s</span><span class="w">
</span></span></span></code></pre></div><p>The dependabot config looks as follows:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yml" data-lang="yml"><span class="line"><span class="cl"><span class="c"># .github/dependabot.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">updates</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">package-ecosystem</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;pip&#34;</span><span class="w"> </span><span class="c"># See documentation for possible values.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">directory</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;/&#34;</span><span class="w"> </span><span class="c"># Location of package manifests.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">schedule</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;daily&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Maintain dependencies for GitHub Actions.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">package-ecosystem</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;github-actions&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">directory</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;/&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">schedule</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;daily&#34;</span><span class="w">
</span></span></span></code></pre></div><h2 id="further-reading">Further reading</h2>
<ul>
<li><a href="https://github.com/rednafi/stress-test-locust/blob/master/.github/workflows/build_test.yml" rel="noopener noreferrer" target="_blank">An active version of the above workflow</a></li>
</ul>
<!-- references -->
<!-- prettier-ignore-start -->
<!-- prettier-ignore-end -->
]]></content:encoded>
    </item>
  </channel>
</rss>
