<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Blog | Mani Soundararajan</title><link>https://msound.net/blog/</link><atom:link href="https://msound.net/blog/index.xml" rel="self" type="application/rss+xml"/><description>Blog</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en-us</language><lastBuildDate>Wed, 19 Jun 2024 00:00:00 +0000</lastBuildDate><image><url>https://msound.net/media/icon_hu68170e94a17a2a43d6dcb45cf0e8e589_3079_512x512_fill_lanczos_center_3.png</url><title>Blog</title><link>https://msound.net/blog/</link></image><item><title>Visualizing Software Architecture using C4 Model and Python Diagrams</title><link>https://msound.net/blog/2024/06/c4-model/</link><pubDate>Wed, 19 Jun 2024 00:00:00 +0000</pubDate><guid>https://msound.net/blog/2024/06/c4-model/</guid><description>&lt;h2 id="c4-model">C4 Model&lt;/h2>
&lt;p>The &lt;strong>C4 Model&lt;/strong> is a great way to explain software architecture using diagrams.&lt;/p>
&lt;p>Instead of throwing the developer into a confusing jumble of boxes and lines, the C4 Model breaks down the architecture starting from the top layer down to the lowest code layer. The C4 model gets it&amp;rsquo;s name from the 4 C&amp;rsquo;s:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Context&lt;/strong> - explain at high level what the software does&lt;/li>
&lt;li>&lt;strong>Container&lt;/strong> - explain how various components come together in the software&lt;/li>
&lt;li>&lt;strong>Component&lt;/strong> - explain each component in detail&lt;/li>
&lt;li>&lt;strong>Code&lt;/strong> - explain the code implementation&lt;/li>
&lt;/ol>
&lt;p>For a detailed explanation, look no further than the official &lt;a href="https://c4model.com/" target="_blank" rel="noopener">C4 Model&lt;/a> website.&lt;/p>
&lt;h2 id="diagramming-tools">Diagramming tools&lt;/h2>
&lt;p>There are several options when it comes to creating diagrams.&lt;/p>
&lt;p>One of the popular options is to create the diagrams in &lt;a href="https://www.drawio.com/" target="_blank" rel="noopener">draw.io&lt;/a>. This tool allows you to export your diagram as an image (say, a PNG) as well as download the diagram in a &lt;code>.drawio&lt;/code> file format - which you can add to your git repository.&lt;/p>
&lt;p>Having the .drawio file ensures that it is possible to make modifications in the future.&lt;/p>
&lt;p>My preferred way to create diagrams is to write a python script using &lt;a href="https://pypi.org/project/diagrams/" target="_blank" rel="noopener">diagrams&lt;/a> module.&lt;/p>
&lt;p>Be sure to checkout the &lt;a href="https://diagrams.mingrammer.com/" target="_blank" rel="noopener">documentation and examples&lt;/a> of the &lt;strong>digrams&lt;/strong> module. It has support for tons of cloud native tools. More specifically, they also have support for the &lt;a href="https://diagrams.mingrammer.com/docs/nodes/c4" target="_blank" rel="noopener">C4 Model&lt;/a>.&lt;/p>
&lt;h2 id="example">Example&lt;/h2>
&lt;p>To see how to create architecture diagrams using &lt;strong>diagrams&lt;/strong>, let&amp;rsquo;s take the case of hypothetical app called Lacerta. Lacerta is used by users to track their safety - by having users regularly &amp;ldquo;check-in&amp;rdquo; to confirm that they are safe. If they miss a check-in, their emergency contact person is informed - who will get in touch with the user and take further action as needed.&lt;/p>
&lt;h3 id="context">Context&lt;/h3>
&lt;p>At the highest level, we have the System Context Diagram:&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Context digram for Lacerta" srcset="
/blog/2024/06/c4-model/context_hu9df90803395c149d51b753807f96062a_45190_eb587075bc842e2415f7bd344be4dc04.webp 400w,
/blog/2024/06/c4-model/context_hu9df90803395c149d51b753807f96062a_45190_e91eee217691e3d336d08fa2f56fd35a.webp 760w,
/blog/2024/06/c4-model/context_hu9df90803395c149d51b753807f96062a_45190_1200x1200_fit_q95_h2_lanczos_3.webp 1200w"
src="https://msound.net/blog/2024/06/c4-model/context_hu9df90803395c149d51b753807f96062a_45190_eb587075bc842e2415f7bd344be4dc04.webp"
width="760"
height="733"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>This diagram was generated by the python code:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">diagrams&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Diagram&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">diagrams.c4&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Person&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">System&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">diagrams&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Node&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">graph_attr&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;splines&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;curves&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="n">Diagram&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Context diagram for Lacerta&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">filename&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;context&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">direction&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;TB&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">graph_attr&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">graph_attr&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Person&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;User&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;A user of Lacerta&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">emerg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Person&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Emergency Contact&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;A friend or family member of the User&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">sys&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">System&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Lacerta System&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user&lt;/span> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;1. configures emergency contacts, and periodically checks-in&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">sys&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;2. reminds if a check-in is missed&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">sys&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">emerg&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;3. alerts if user has missed a check-in despite reminders&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">sys&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;4. manually confirms that the User is ok&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">emerg&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="vm">__name__&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;__main__&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">main&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="container">Container&lt;/h3>
&lt;p>At the second level, the container diagram explains how various components work together internally as well as with external systems. Here is the container diagram for Lacerta:&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Container diagram for Lacerta" srcset="
/blog/2024/06/c4-model/container_hucb67f7fed14cf68d0f2b2c2bc0f2248c_126198_dc8b01499434fd5a4731f67e5a3d3ccd.webp 400w,
/blog/2024/06/c4-model/container_hucb67f7fed14cf68d0f2b2c2bc0f2248c_126198_f3090a7b9766d5740954efbcaf8356a9.webp 760w,
/blog/2024/06/c4-model/container_hucb67f7fed14cf68d0f2b2c2bc0f2248c_126198_1200x1200_fit_q95_h2_lanczos_3.webp 1200w"
src="https://msound.net/blog/2024/06/c4-model/container_hucb67f7fed14cf68d0f2b2c2bc0f2248c_126198_dc8b01499434fd5a4731f67e5a3d3ccd.webp"
width="760"
height="484"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>This diagram was generated by this Python code:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">diagrams&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Diagram&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">diagrams.c4&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Container&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Database&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Person&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">SystemBoundary&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">System&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">diagrams&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Node&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">graph_attr&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;splines&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;curves&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="n">Diagram&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Container diagram for Lacerta System&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">filename&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;container&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">direction&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;TB&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">graph_attr&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">graph_attr&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Person&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;User&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;A user of Lacerta&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">emerg&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Person&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Emergency Contact&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;A friend or family member of the User&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">with&lt;/span> &lt;span class="n">SystemBoundary&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Lacerta application&amp;#34;&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mobile_app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Container&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Mobile App&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">technology&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Flutter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Provides ability for user to check-in periodically, and also to configure emergency contacts&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">backend&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Container&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Backend&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">technology&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Go&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Backend API application allows user to check-in, and configure emergency contacts&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">daemon&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Container&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Daemon&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">technology&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Go&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Constantly checks if any user has missed a check-in&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">db&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Database&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Database&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">technology&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;MongoDB&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Database to store configuration and check-in times&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">email&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">System&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Email&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;SendGrid Transactional Email Service&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">external&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">push&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">System&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;Push Notification&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;Firebase Cloud Messaging&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">external&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user&lt;/span> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;checks-in and configures emergency contacts using&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">mobile_app&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">mobile_app&lt;/span> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;make API calls to&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">backend&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">backend&lt;/span> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;reads from and writes to&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">reverse&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="kc">True&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">db&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">daemon&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;reads from&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">db&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">daemon&lt;/span> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;triggers email reminders or email alerts&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">email&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">daemon&lt;/span> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;triggers push notification reminders to check-in&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;gt;&amp;gt;&lt;/span> &lt;span class="n">push&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;reminder emails to check-in&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">email&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;reminder messages to check-in&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">push&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">emerg&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;alert email about user&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">email&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">user&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">Relationship&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;manual check-in to verify if user is ok&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">&amp;lt;&amp;lt;&lt;/span> &lt;span class="n">emerg&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="vm">__name__&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;__main__&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">main&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you look at the code, it does not look like typical imperative code written in Python, this reads more declarative - in terms of defining boxes, the lines that connect the boxes, and the direction of the arrow.&lt;/p>
&lt;h3 id="component">Component&lt;/h3>
&lt;p>I do not create diagrams at the Component level. You can, if you wish create diagrams for each component. As you get lower down the layers, you may find yourself changing the diagrams a lot during heavy development periods. So creating a diagram for the Component layer may or may not make sense for your team.&lt;/p>
&lt;h3 id="code">Code&lt;/h3>
&lt;p>You shoud almost never create diagrams for the Code layer - as we expect the code to change a lot over the lifecycle of the software. We can strive to writing clean code so its easier to read the code - or use automated tools to parse the code and generate diagrams.&lt;/p>
&lt;h2 id="reasoning">Reasoning&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>I find that using GUI based diagramming tools gives me way too much choice - do I want rounded corners for the rectangles, or should they be sharp corners. What background color should I use for a database box, and which of the ten different arrow styles to use. It is hard to stay consistent when creating many diagrams. With code based diagramming tools, a lot of that choice is taken away from me - so I can focus on which component interacts with which component, and in what way.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>Creating diagrams using code allows me to commit both the diagram, and the source code into a git repository - bringing it under version control. If a question arises in the future about when a certain architectural change was introduced, we can easily figure this out using &lt;code>git log&lt;/code> and &lt;code>git blame&lt;/code>. (See also: &lt;a href="https://msound.net/blog/2024/02/architecture-decision-records/" target="_blank" rel="noopener">Architecture Decision Records&lt;/a>)&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://c4model.com/" target="_blank" rel="noopener">C4 Model&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://diagrams.mingrammer.com/" target="_blank" rel="noopener">Diagrams module&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/msound/lacerta" target="_blank" rel="noopener">github.com/msound/lacerta&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Guess Which Door Has A Car Behind It</title><link>https://msound.net/blog/2024/06/monty-hall-problem/</link><pubDate>Thu, 13 Jun 2024 00:00:00 +0000</pubDate><guid>https://msound.net/blog/2024/06/monty-hall-problem/</guid><description>&lt;p>There are three doors. Behind one of the doors is a car, and behind the other two are goats. Can you beat the odds, guess the correct door and win the car?&lt;/p>
&lt;p>This problem is popularly known as the &lt;strong>Monty Hall problem&lt;/strong> - named after the TV show host who ran Let&amp;rsquo;s Make a Deal back in the 60s and 70s. It goes like this: Monty asks you to pick a door. Let&amp;rsquo;s say you pick Door 1. Then Monty goes behind each door and sees where the car is, and where the goats are. He then proceeds to open, let&amp;rsquo;s say, Door 3 and reveals a goat. Monty now offers you a choice - do you want to &lt;strong>STAY&lt;/strong> with Door 1, or do you want to &lt;strong>SWITCH&lt;/strong> to Door 2.&lt;/p>
&lt;p>Think about this for a second. And think in terms of probability - not in terms of I&amp;rsquo;m feeling lucky about Door 1 or Door 2 etc. What would you do?&lt;/p>
&lt;p>I came across this problem when I went into a rabbit hole after watching a YouTube clip of Kevin Spacey (playing a professor) quizzing his class about this exact problem.&lt;/p>
&lt;p>Here was my thinking: Now that Door 3 is eliminated, it&amp;rsquo;s a 50/50 toss up between Door 1 and Door 2. So it does not really matter if I stay or switch. I have no statistical advantage in doing either. In fact, I think I was &lt;strong>lucky&lt;/strong> in not picking Door 3, and that I should let my lucky run continue and not mess things up - so I am going to &lt;strong>STAY&lt;/strong> with Door 1. Turns out, this is flawed thinking! And it&amp;rsquo;s not just me, some really &lt;a href="https://web.archive.org/web/20160306152254/http://www.nytimes.com/1991/07/21/us/behind-monty-hall-s-doors-puzzle-debate-and-answer.html?pagewanted=3&amp;amp;src=pm" target="_blank" rel="noopener">good mathematicians&lt;/a> also argued that the odds are 50/50 between Door 1 and Door 2.&lt;/p>
&lt;p>But first, I want you to try this for yourself. Below is a simulation of the Monty Hall problem. I want you to play this with two different strategies. First, I want you to play the &lt;strong>STAY&lt;/strong> strategy. With this, you will pick a door, and when the goat is revealed, you will &lt;strong>STAY&lt;/strong> with the same door you first picked. Playing this is easy, just keep clicking on the same door - again and again. At the end of the game, click anywhere to restart. To truly see the statistics play out, you need to play this at least 30 times. Next I want you to play the &lt;strong>SWITCH&lt;/strong> strategy. For this, you will pick any door, and when the goat is revealed, you will &lt;strong>SWITCH&lt;/strong> to the other remaining door. Again, play this about 30 times. Please indulge me, you need to see this for yourself to believe it:&lt;/p>
&lt;div id="app">
&lt;div id="doors">
&lt;div class="door" id="door0" onclick="javascript:openDoor(0);">🚪&lt;/div>
&lt;div class="door" id="door1" onclick="javascript:openDoor(1);">🚪&lt;/div>
&lt;div class="door" id="door2" onclick="javascript:openDoor(2);">🚪&lt;/div>
&lt;/div>
&lt;div id="message">
&lt;p>Pick a door.&lt;/p>
&lt;/div>
&lt;div id="results">
&lt;table id="results">
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Win&lt;/th>
&lt;th>Lose&lt;/th>
&lt;th>Win %&lt;/th>
&lt;/tr>
&lt;tr>
&lt;td>Stay&lt;/td>
&lt;td id="stay-win" class="win">0&lt;/td>
&lt;td id="stay-lose" class="lose">0&lt;/td>
&lt;td id=stay-percent>0%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Switch&lt;/td>
&lt;td id="switch-win" class="win">0&lt;/td>
&lt;td id="switch-lose" class="lose">0&lt;/td>
&lt;td id="switch-percent">0%&lt;/td>
&lt;/tr>
&lt;/table>
&lt;/div>
&lt;/div>
&lt;script type="text/javascript">
var states = ['door', 'door', 'door'];
var emojis = {
'door': '🚪',
'goat': '🐐',
'car': '🚗'
}
var carIndex = getRandomIndex();
var revealIndex = null;
var firstChoice = null;
var gameState = 0;
var results = {
'stay': {
'win': 0,
'lose': 0
},
'switch': {
'win': 0,
'lose': 0
}
};
window.onload = function() {
renderStates();
}
function renderStates() {
states.forEach(function(state, index) {
var door = document.getElementById('door' + index);
door.innerHTML = emojis[state];
});
}
function openDoor(index) {
if (gameState === 0) {
if (index === carIndex) {
var goatIndices = [0, 1, 2].filter(function(i) {
return i !== carIndex;
});
var goatIndex = goatIndices[Math.floor(Math.random() * 2)];
states[goatIndex] = 'goat';
revealIndex = goatIndex;
} else {
var goatIndices = [0, 1, 2].filter(function(i) {
return i !== carIndex &amp;&amp; i !== index;
});
var goatIndex = goatIndices[0];
states[goatIndex] = 'goat';
revealIndex = goatIndex;
}
gameState = 1;
firstChoice = index;
renderStates();
updateMessage('Stay or switch?');
}
else if (gameState === 1) {
if (index === revealIndex) {
return;
}
states[0] = carIndex === 0 ? 'car' : 'goat';
states[1] = carIndex === 1 ? 'car' : 'goat';
states[2] = carIndex === 2 ? 'car' : 'goat';
renderStates();
if (index === carIndex) {
if (firstChoice === index) {
results.stay.win++;
highlightResult('stay', 'win');
} else {
highlightResult('switch', 'win');
results.switch.win++;
}
updateMessage('😎 You win! Click anywhere to play again.');
} else {
if (firstChoice === index) {
highlightResult('stay', 'lose');
results.stay.lose++;
} else {
highlightResult('switch', 'lose');
results.switch.lose++;
}
updateMessage('😢 You lose! Click anywhere to play again.');
}
gameState = 2;
updateResults();
}
else if (gameState === 2) {
states = ['door', 'door', 'door'];
carIndex = getRandomIndex();
gameState = 0;
firstChoice = null;
revealIndex = null;
renderStates();
clearHighlights();
updateMessage('Pick a door.');
}
}
function updateResults() {
var stayWin = document.getElementById('stay-win');
var stayLose = document.getElementById('stay-lose');
var stayPercent = document.getElementById('stay-percent');
var switchWin = document.getElementById('switch-win');
var switchLose = document.getElementById('switch-lose');
var switchPercent = document.getElementById('switch-percent');
stayWin.innerHTML = results.stay.win;
stayLose.innerHTML = results.stay.lose;
if (results.stay.win + results.stay.lose === 0) {
stayPercent.innerHTML = '0%';
} else {
stayPercent.innerHTML = (results.stay.win / (results.stay.win + results.stay.lose) * 100).toFixed(2) + '%';
}
switchWin.innerHTML = results.switch.win;
switchLose.innerHTML = results.switch.lose;
if (results.switch.win + results.switch.lose === 0) {
switchPercent.innerHTML = '0%';
} else {
switchPercent.innerHTML = (results.switch.win / (results.switch.win + results.switch.lose) * 100).toFixed(2) + '%';
}
}
function highlightResult(scenario, win) {
var td = document.getElementById(scenario + '-' + win);
td.classList.add('highlight');
}
function clearHighlights() {
var tds = document.querySelectorAll('td.highlight');
tds.forEach(function(td) {
td.classList.remove('highlight');
});
}
function updateMessage(message) {
var messageDiv = document.getElementById('message');
messageDiv.innerHTML = '&lt;p>' + message + '&lt;/p>';
}
function getRandomIndex() {
const a = new Uint8Array(1);
window.crypto.getRandomValues(a);
return a[0] % 3;
}
&lt;/script>
&lt;style type="text/css">
#app {
width: 600px;
text-align: center;
}
#message {
font-size: 80%;
}
.door {
display: inline-block;
font-size: 4em;
margin: 0 20px;
cursor: pointer;
}
table#results {
margin-top: 20px;
border-collapse: collapse;
}
table#results th, table#results td {
padding: 5px 20px;
text-align: center;
}
table#results td.win.highlight {
background-color: lightgreen;
}
table#results td.lose.highlight {
background-color: lightcoral;
}
&lt;/style>
&lt;p>If you played this a lot of times, like 30 times for each strategy, you will most likely see about a 33% win rate for the &lt;strong>STAY&lt;/strong> strategy, and about a 67% win rate for the &lt;strong>SWITCH&lt;/strong> strategy. Shouldn&amp;rsquo;t it be 50/50 between the two strategies? What is happening?&lt;/p>
&lt;p>To get our heads around this problem, let&amp;rsquo;s remove &amp;ldquo;you&amp;rdquo; from the equation and replace &amp;ldquo;you&amp;rdquo; with &amp;ldquo;Alice&amp;rdquo;. Alice is the player and she will be the one choosing the doors. For every choice Alice makes, there is the choice she did not make - the philosophical &amp;ldquo;road not taken&amp;rdquo; - let&amp;rsquo;s personify this as Bob. Let&amp;rsquo;s say that Bob is also a player, and he does NOT get to pick a door - he simply gets the doors that Alice does not pick.&lt;/p>
&lt;p>So, if Alice picks Door 1, then Bob gets Door 2 and Door 3. At this point, Alice has a 33% probability of winning a car, and Bob has a 67% probability.&lt;/p>
&lt;p>So, initially, when Alice picks Door 1, the probability of her winning is 33% and the probability of Bob (with Door 2 and Door 3) winning is 67%. Let&amp;rsquo;s call this STAGE 1. Then a goat is revealed in Door 3, and Alice is offered a choice to &lt;strong>STAY&lt;/strong> with Door 1 or &lt;strong>SWITCH&lt;/strong> to Door 2, let&amp;rsquo;s call that STAGE 2. Note that between STAGE 1 and STAGE 2, Monty did not SHUFFLE the car and goat behind Door 1 and Door 2. During the entire game, once the car is placed behind a Door - it does not move. As long as there is no SHUFFLING, then Bob&amp;rsquo;s odds of 67% does not change either.&lt;/p>
&lt;p>But why doesn&amp;rsquo;t Bob&amp;rsquo;s probability go down from 67% to 50%? Because no new information has been revealed that changes that fact. There is still only ONE car, and there are three doors. Bob has 2 doors (doesn&amp;rsquo;t matter if it&amp;rsquo;s open or closed). So Bob&amp;rsquo;s probability stays at 67%. When Monty reveals the goat in Door 3, he is playing a trick. He is telling the audience, &amp;ldquo;Look! Bob has a goat behind one of his doors&amp;rdquo; - which really should surprise no one. Bob either has a car and a goat, or two goats. Saying &amp;ldquo;Bob has one goat&amp;rdquo; does not in any way change Bob&amp;rsquo;s odds of winning. The fact that Door 3 has a goat has strengthened Bob&amp;rsquo;s odds of having a car behind Door 2 - because keep in mind, Door 2 + Door 3 is 67%. So if Door 3 is a bust, then all that 67% probability goes to Door 2.&lt;/p>
&lt;p>In other words, asking Alice if she wants to &lt;strong>SWITCH&lt;/strong> to Door 2 is the same as asking if she wants to trade places with Bob, and his 67% probability of winning a car. If we put it that way, then we see that it makes sense to &lt;strong>SWITCH&lt;/strong>.&lt;/p>
&lt;p>Lastly, I want to go back to your original intuition of thinking the choice is 50/50 because there are only two doors left. That is true ONLY IF there is SHUFFLING of car and goat after STAGE 1. If there is SHUFFLING, then all bets are off. At that point, it does not make any statistical difference if you &lt;strong>STAY&lt;/strong> or &lt;strong>SWITCH&lt;/strong>.&lt;/p>
&lt;p>Note that if you are playing the &lt;strong>SWITCH&lt;/strong> strategy, you may still lose - that is why the odds are 67% and not 100%. No such thing as a free car, eh?&lt;/p>
&lt;hr>
&lt;p>Hey, if you&amp;rsquo;ve made it this far, I have two favors to ask of you:&lt;/p>
&lt;ol>
&lt;li>
&lt;p>If you are a programmer, please go ahead and use your Github account to add reactions or comments below.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>If you liked this post, please share this with others!&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://en.wikipedia.org/wiki/Monty_Hall_problem" target="_blank" rel="noopener">Wikipedia: Monty Hall Problem&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/msound/montyhall" target="_blank" rel="noopener">github.com/msound/montyhall&lt;/a>&lt;/li>
&lt;/ul>
&lt;hr>
&lt;p>&lt;strong>Note to programmers&lt;/strong>: When I was working on this simulation game, I wanted to make sure it was fair. Which means, if you play the game 30 times, irrespective of whether you win or not, the car should be behind Door 1 approximately 10 times, and behind Door 2 approximately ten times, and behind Door 3 approximately ten times. That is what you would expect in a pure random distribution. When I used &lt;code>Math.random()&lt;/code> I found that the results were wildly skewed. So, for my game I switched to &lt;code>crypto.getRandomValue()&lt;/code>. But that is not perfect either - because computers don&amp;rsquo;t really do random - they only do pseudorandom. Even then, my implementation has an &amp;ldquo;unfairness&amp;rdquo; to it. Read the function &lt;code>getRandomIndex()&lt;/code> in my &lt;a href="https://github.com/msound/montyhall" target="_blank" rel="noopener">source code&lt;/a>, and tell me in comments below if you can spot what is unfair about it!&lt;/p></description></item><item><title>Developing Software For 25 Years - A Retrospective</title><link>https://msound.net/blog/2024/06/restrospective-25-years/</link><pubDate>Sat, 01 Jun 2024 00:00:00 +0000</pubDate><guid>https://msound.net/blog/2024/06/restrospective-25-years/</guid><description>&lt;p>This week I am completing 25 years of my software development career. I figured this major milestone deserves a retrospective. Here are some of the lessons I have learned over the years:&lt;/p>
&lt;h2 id="1-free-and-open-source-software-makes-the-world-go-around">1. Free and Open Source Software makes the world go around&lt;/h2>
&lt;p>Without going into details of copyleft licenses (like GPL) or permissive licenses (like BSD, MIT), I will say that free and open source software (FOSS) runs a lot of things on this planet. FOSS powers a &lt;a href="https://www.netcraft.com/blog/may-2024-web-server-survey/" target="_blank" rel="noopener">good part&lt;/a> of the internet. Governments around the world are switching to FOSS. Heck, there&amp;rsquo;s FOSS running &lt;a href="https://spectrum.ieee.org/nasa-designed-perseverance-helicopter-rover-fly-autonomously-mars" target="_blank" rel="noopener">off-planet&lt;/a> as well. Much of the tech boom we have seen in the last two decades was made possible because we are standing on the shoulders of FOSS giants.&lt;/p>
&lt;p>Over the last 17 years, I have made several &lt;a href="https://www.drupal.org/u/msound/issue-credits" target="_blank" rel="noopener">contributions&lt;/a> in my own &lt;a href="https://github.com/grafana/grafana/pull/6481" target="_blank" rel="noopener">small&lt;/a> way. I also got an opportunity to speak about my cool (at that time) 5000+ docker container production deployment at OpenCamps 2017 held at the United Nations in New York.&lt;/p>
&lt;p>At this 25th year of my tech career, I am more confident than ever about the future of FOSS, and I am doubling my resolve to spend more of my time using FOSS, promoting FOSS and contributing to FOSS.&lt;/p>
&lt;h2 id="2-reading-is-a-superpower">2. Reading is a superpower&lt;/h2>
&lt;p>In this era of attention attrition, I have found that my ability to read is a superpower. I am able to answer so many of my colleagues&amp;rsquo; questions simply because I have read the documentation cover-to-cover. Other times, I enlarge my knowledge base by NOT going to stackoverflow or reddit seeking a quick answer, but by reading official documentation, API reference and even upstream source code.&lt;/p>
&lt;p>Reading has several obvious advantages to watching a video. You can usually skim a page (or even screenful) of information faster than it takes for you to watch a video about the same topic. Also, reading has &amp;ldquo;random access&amp;rdquo; capabilities. You can skip a few pages or go back. With video, cueing forward or reverse does not have the same quality of experience.&lt;/p>
&lt;p>This superpower extends to reading in general. To parents of young children, I will say this: children learn by observing you more than by listening to you. So if you want your kids to read, you should be seen reading books, preferably the print kind.&lt;/p>
&lt;h2 id="3-saying-yes-to-more-things-leads-to-interesting-experiences">3. Saying Yes to more things leads to interesting experiences&lt;/h2>
&lt;p>I have worked more than 20 years in startups. A startup career does not come with fixed job titles or responsibilities. Over the years, anytime I got the opportunity to step outside my comfort zone, I have said yes - and every time I have come away gaining a lot of experience.&lt;/p>
&lt;p>I said yes to working on embedded firmware, and learned that I can apply my skills to solve any problem - including finding a runaway pointer in a ring buffer that caused memory overwrites and sleepless nights. I said yes to working on a Radio Frequency communication project, and learned that the random packet interference I was receiving was just folks opening their cars with their key fobs. I said yes to working customer support and learned that my customers were using my product in ways I did not intend or even imagine.&lt;/p>
&lt;h2 id="4-empathy-is-the-missing-piece-in-most-software-stacks">4. Empathy is the missing piece in most software stacks&lt;/h2>
&lt;p>I have been asked many times about what I look for when hiring someone. I always say Empathy and Motivation. Our world (and our technologies) will be a whole lot better with some empathy.&lt;/p>
&lt;p>Be more empathetic to your customers - they are the ones putting the food on your table. Be more empathetic to your colleagues - they are the ones sharing your responsibilities. Be more empathetic to your frontline customer support - they are the ones facing angry customers when you break production. And lastly be more empathetic to future developers who will be inheriting your code base.&lt;/p>
&lt;h2 id="5-doing-customer-support-improves-empathy">5. Doing customer support improves empathy&lt;/h2>
&lt;p>Some of the most insightful learnings about my product have come from doing customer support.&lt;/p>
&lt;p>Customers have this uncanny ability to bend your product to their will to achieve their objectives. If you are not paying attention to this, sooner or later you will make a change in your product that breaks the customer&amp;rsquo;s workflow. Remember, backward compatibility is not just for your published API - but for all of your software&amp;rsquo;s behaviors that the customer is using in their workflow.&lt;/p>
&lt;p>When you can, try to spend a rotation doing customer support.&lt;/p>
&lt;h2 id="6-find-your-rhythm-for-doing-boring-repetitive-tasks">6. Find your rhythm for doing boring repetitive tasks&lt;/h2>
&lt;p>We have incredibly interesting jobs - but that comes with sometimes having to do boring repetitive tasks. This is work that is not appealing to do, but needs to be done nonetheless. Sometimes it might make sense to spend the time automating or scripting a solution - but most times it’ll make sense to grind through the chore.&lt;/p>
&lt;p>I have learned that it is important to find my rhythm when doing such tasks. If I go too slow, I will zone out and lose focus. If I go too fast, I will end up making mistakes and spend even more time cleaning up my mess. Things I have found helpful are making checklists and checking items off at a steady pace.&lt;/p>
&lt;p>Like the US Navy Seals say: “Slow is smooth, smooth is fast”.&lt;/p>
&lt;h2 id="7-figure-out-how-your-brain-works">7. Figure out how your brain works&lt;/h2>
&lt;p>One of the first lessons we learn as students in a laboratory course is called “study of tools”. If this is an electronics lab course, you will learn about voltmeters, ammeters etc. If this is a carpentry shop course, you will learn about t-squares, various kinds of saws and so on.&lt;/p>
&lt;p>What are the tools of our trade? Keyboard and mouse? Sure we all know how to use them. Programming languages, IDEs, compiler toolchains? Yes, we all know that too. But we don’t really think of our brain as being part of this - and yet it is the most important tool in our toolkit.&lt;/p>
&lt;p>As software developers, we owe it to ourselves to try and understand our brains a little better. How does my brain perform after a carb-heavy meal (not good), and how does it perform after going for a run (much better).&lt;/p>
&lt;p>That is why I find topics like &lt;a href="https://msound.net/tag/cognitive-load/" target="_blank" rel="noopener">Cognitive Load&lt;/a> fascinating. That is the reason why I ask my team to keep pull requests to less than 200 lines of diff so that the code reviewer does not suffer cognitive overload (see point #4 above regarding empathy towards colleagues).&lt;/p>
&lt;h2 id="8-sleep-is-an-important-step-in-debugging">8. Sleep is an important step in debugging&lt;/h2>
&lt;p>There is no bug you can&amp;rsquo;t tackle &lt;strong>after&lt;/strong> you&amp;rsquo;ve had a good night&amp;rsquo;s sleep.&lt;/p>
&lt;p>Again, to understand this, you will need to understand how your brain works. The more awake you are, the more toxins build up in your brain - and the only way to flush them out of your system is to &lt;a href="https://www.ninds.nih.gov/health-information/public-education/brain-basics/brain-basics-understanding-sleep" target="_blank" rel="noopener">sleep&lt;/a>. And it&amp;rsquo;s not like you have stopped debugging the problem you were working on. In fact, the work continues in your subconsciousness. The NREM cycle of your sleep moves information from temporary memory (hypothalamus) to permanent memory (cerebrum), and the REM cycle creates links between neurons representing new information and neurons representing information you already had in your brain. This &amp;ldquo;linking&amp;rdquo; process is what creativity is - and this is going to help you come up with creative ways of debugging the problem.&lt;/p>
&lt;p>I would highly recommend Dr. Matthew Walker&amp;rsquo;s book &amp;ldquo;Why We Sleep&amp;rdquo;. And I would also recommend getting something like a fitbit and wearing it while sleeping so you can start seeing patterns of your mood / performance vs. your sleep quality.&lt;/p>
&lt;p>So, if you are not able to fix a bug after working on it for several hours - call it a day and go to sleep. You will wake up with fresh perspectives on how to approach your problem. Now, if you have figured out how to account for that in your timesheet, please let me know!&lt;/p>
&lt;h2 id="9-if-you-dont-like-your-organizations-culture-change-it">9. If you don&amp;rsquo;t like your organization&amp;rsquo;s culture, change it&lt;/h2>
&lt;p>We all like to say work culture is a really important factor when considering joining an organization. But what is good work culture? For a really good definition of what makes a good work culture - look no further than the book Accelerate by Dr. Nicole Forsgren, Jez Humble and Gene Kim - skip to Chapter 3 to read about this topic - or better yet read the entire book. To paraphrase the authors quickly, there are &amp;ldquo;pathological workplaces&amp;rdquo;, &amp;ldquo;bureaucratic workplaces&amp;rdquo; and then there is &amp;ldquo;generative workplace&amp;rdquo; - which is what we all think of as &amp;ldquo;good work culture&amp;rdquo;.&lt;/p>
&lt;p>Setting the organization&amp;rsquo;s culture is not something that is the sole domain of the manager - everyone can and should work towards improving the work culture. If you work in a small company, there is no reason why you shouldn&amp;rsquo;t be able to implement changes. Even if you work in a large organization, you can bring about changes in your team.&lt;/p>
&lt;h2 id="10-in-software-ethics-you-are-the-last-line-of-defense">10. In software ethics, you are the last line of defense&lt;/h2>
&lt;p>Technology has taken over much of the world - and I have seen this just in my lifetime. Software runs pretty much everything - and as software developers we have great power. We can either use this power to build features that will benefit our customers, or we can abuse this power to build anti-features that are not in the best interest of our customers.&lt;/p>
&lt;p>Without going into specifics, I can say that I have, in the past, pushed back on dubious feature requests - and the management in each case agreed to drop the feature.&lt;/p>
&lt;p>I am here to say today to you - that you have the agency to refuse to work on anti-features. This is not something you have to earn with seniority - you get this on day one.&lt;/p>
&lt;p>Remember what uncle Ben said: &amp;ldquo;With great power comes great responsibility&amp;rdquo;.&lt;/p></description></item><item><title>Version bumping with Uplift</title><link>https://msound.net/blog/2024/05/version-bumping/</link><pubDate>Thu, 23 May 2024 00:00:00 +0000</pubDate><guid>https://msound.net/blog/2024/05/version-bumping/</guid><description>&lt;p>Making a software release involves a bunch of boring routine tasks - with &lt;a href="https://upliftci.dev/" target="_blank" rel="noopener">Uplift&lt;/a> you can automate all of it.&lt;/p>
&lt;h2 id="uplift">Uplift&lt;/h2>
&lt;p>With Uplift, you can do three things at once:&lt;/p>
&lt;ol>
&lt;li>Bump the version number&lt;/li>
&lt;li>Update your changelog&lt;/li>
&lt;li>Tag and push&lt;/li>
&lt;/ol>
&lt;p>And all it takes is to run:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ uplift release
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://upliftci.dev/install/binary/" target="_blank" rel="noopener">Uplift installation instructions&lt;/a>&lt;/p>
&lt;h2 id="conventional-commits">Conventional commits&lt;/h2>
&lt;p>Before you can leverage Uplift, you should ensure all your git commit messages are following the &lt;a href="https://www.conventionalcommits.org/en/v1.0.0/" target="_blank" rel="noopener">Official Conventional Commits&lt;/a> standard. You can also refer to my post on &lt;a href="https://msound.net/blog/2023/10/conventional-commits/">conventional commits&lt;/a>.&lt;/p>
&lt;p>When you write commit messages like &lt;code>feat: add payment module&lt;/code> or &lt;code>fix: update payment flow&lt;/code>, Uplift is able to parse those messages and determine whether the number needs to be bumped at major, minor or patch version.&lt;/p>
&lt;p>If all your changes are &lt;code>fix&lt;/code> type commits, Uplift will bump the patch version.&lt;/p>
&lt;p>If one your changes is a &lt;code>feat&lt;/code> type commit, Uplift will bump the minor version.&lt;/p>
&lt;p>If any of your commits have breaking change, like &lt;code>fix!&lt;/code> or &lt;code>feat!&lt;/code>, Uplift will bump the major version.&lt;/p>
&lt;p>Other than this, Uplift is also able to parse commit messages and come up with a Changelog for the current release. This will be automatically updated in a &lt;code>Changelog.md&lt;/code> file.&lt;/p>
&lt;h2 id="configuration">Configuration&lt;/h2>
&lt;p>Uplift expects the configuration to be stored in a file like &lt;code>.uplift.yaml&lt;/code> (there are some other file name patterns that are supported). This file will contain configuration related to which file version needs to be bumped and so on.&lt;/p>
&lt;p>Let&amp;rsquo;s look at an example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># uplift.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># see https://upliftci.dev/&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">bumps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">main.go&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">regex&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pattern&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;const appVersion = &amp;#34;$VERSION&amp;#34;&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">semver&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">annotatedTags&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">changelog&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exclude&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;chore&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;refactor&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;style&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;test&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;wip&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">commitMessage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;chore(release): $VERSION&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This file instructs Uplift to bump the version in main.go in a line that matches the regex pattern &lt;code>const appVersion = &amp;quot;$VERSION&amp;quot;&lt;/code>. For example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="s">&amp;#34;log&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">const&lt;/span> &lt;span class="nx">appVersion&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;v0.1.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Helloworld&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">appVersion&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To this, you add a bug fix and change &lt;code>Helloworld&lt;/code> into two words &lt;code>Hello world&lt;/code>. Make sure the commit messages says something like &lt;code>fix: split helloworld into two words&lt;/code>. Now if you run &lt;code>uplift release&lt;/code>, you would get:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="s">&amp;#34;log&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">const&lt;/span> &lt;span class="nx">appVersion&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;v0.1.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Helloworld&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">appVersion&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Uplift has bumped the patch version. It has also added a section to the Changelog.md file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-md" data-lang="md">&lt;span class="line">&lt;span class="cl">&lt;span class="gu">## [v0.1.1]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="gu">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">-&lt;/span> fix: split helloworld into two words
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And lastly, Uplift has created a git tag &lt;code>v0.1.1&lt;/code> and pushed it to the git remote origin.&lt;/p>
&lt;h2 id="demo">Demo&lt;/h2>
&lt;p>I have created a repo to demonstrate conventional commits and uplift:&lt;/p>
&lt;p>&lt;a href="https://github.com/msound/example-helloworld" target="_blank" rel="noopener">Github repo&lt;/a>&lt;/p>
&lt;p>&lt;a href="https://github.com/msound/example-helloworld/commit/373d0cd2f32790e8016bbad92a25ade47da91906" target="_blank" rel="noopener">Commit created by uplift&lt;/a>&lt;/p>
&lt;p>&lt;a href="https://github.com/msound/example-helloworld/tags" target="_blank" rel="noopener">Tag created by uplift&lt;/a>&lt;/p>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.conventionalcommits.org/en/v1.0.0/" target="_blank" rel="noopener">Official Conventional Commits website&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://semver.org/" target="_blank" rel="noopener">Semantic versioning&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://upliftci.dev/reference/config/" target="_blank" rel="noopener">Uplift documentation&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/msound/example-helloworld" target="_blank" rel="noopener">example-helloworld github repo&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://msound.net/blog/2023/10/conventional-commits/">My earlier blog post on conventional commits&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Managing context switches</title><link>https://msound.net/blog/2024/05/managing-context-switches/</link><pubDate>Sat, 11 May 2024 00:00:00 +0000</pubDate><guid>https://msound.net/blog/2024/05/managing-context-switches/</guid><description>&lt;p>There are some days when I am able to turn off my slack and get a blissful few hours of solid productivity. But then there are other days filled with knock-knock-brush sounds from slack, meetings, and other interruptions that bog me down with constant context switches.&lt;/p>
&lt;h2 id="congnitive-load-of-interruptions">Congnitive load of interruptions&lt;/h2>
&lt;p>Let&amp;rsquo;s say you are breezing along on your task when suddenly you are interrupted by a team member requesting you to do something else. Now you pause your task, and go about helping your team mate. When that is done, you go back to your original task - at which point, you are probably asking yourself &amp;ldquo;Wait, what was I doing?&amp;rdquo;. Every time you change your focus from one task to another, it involves a context switch. Your brain has to start loading a whole bunch of related memories in order to start working (or resume working) on a task. This is one example of cognitive load.&lt;/p>
&lt;p>A lot has been written about the cognitive load of context switches. Studies have shown it can take up to 23 minutes to fully recover your flow after an interruption.&lt;/p>
&lt;h2 id="enter-todo">Enter TODO+&lt;/h2>
&lt;p>&lt;a href="https://marketplace.visualstudio.com/items?itemName=fabiospampinato.vscode-todo-plus" target="_blank" rel="noopener">TODO+&lt;/a> is an extension for VS Code that allows you to manage TODO lists, and store them in a file named &lt;code>TODO&lt;/code> as part of your project repo.&lt;/p>
&lt;p>When you are setting out on a task, say a pull request for an issue, start by breaking it down into smaller tasks and put them in your TODO file.&lt;/p>
&lt;div class="flex px-4 py-3 rounded-md bg-primary-100 dark:bg-primary-900">
&lt;span class="pr-3 pt-1 text-primary-400">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.25 11.25l.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9-3.75h.008v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">Pro-tip: Always keep the TODO file open in a right side pane, so you can quickly glance at your current task.&lt;/span>
&lt;/div>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="TODO file on right pane" srcset="
/blog/2024/05/managing-context-switches/01-todo-right-pane-dark_hu74ebe92ed27dff89184670d1c7775d3e_63149_6f033480591e9a1fd2cf145d5095b78c.webp 400w,
/blog/2024/05/managing-context-switches/01-todo-right-pane-dark_hu74ebe92ed27dff89184670d1c7775d3e_63149_1aabbd30cf378c10a53f23e68a31c00f.webp 760w,
/blog/2024/05/managing-context-switches/01-todo-right-pane-dark_hu74ebe92ed27dff89184670d1c7775d3e_63149_1200x1200_fit_q95_h2_lanczos_3.webp 1200w"
src="https://msound.net/blog/2024/05/managing-context-switches/01-todo-right-pane-dark_hu74ebe92ed27dff89184670d1c7775d3e_63149_6f033480591e9a1fd2cf145d5095b78c.webp"
width="760"
height="299"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>If you have to navigate to the TODO file and open it, you are already switching context from your currently open file to the TODO file.&lt;/p>
&lt;p>If you can&amp;rsquo;toggle-start-darkt get the checkbox symbol to show up on your task, you can either hit Alt+Enter, (or right click and choose &amp;ldquo;Toggle Box&amp;rdquo; from the context menu).&lt;/p>
&lt;p>Before starting a task, click on your first task and hit Alt+S (or Toggle Start from the context menu).&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Toggle start in context menu" srcset="
/blog/2024/05/managing-context-switches/02-toggle-start-dark_hu8973f91e8e1a6904f6badc3692c2f5ab_39385_0388839bddf218dcbf149347701ab4e8.webp 400w,
/blog/2024/05/managing-context-switches/02-toggle-start-dark_hu8973f91e8e1a6904f6badc3692c2f5ab_39385_f96cf5474d3adcd2a5f2313b11029dd2.webp 760w,
/blog/2024/05/managing-context-switches/02-toggle-start-dark_hu8973f91e8e1a6904f6badc3692c2f5ab_39385_1200x1200_fit_q95_h2_lanczos_3.webp 1200w"
src="https://msound.net/blog/2024/05/managing-context-switches/02-toggle-start-dark_hu8973f91e8e1a6904f6badc3692c2f5ab_39385_0388839bddf218dcbf149347701ab4e8.webp"
width="468"
height="608"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>If you are interrupted and come back to your VS Code, a quick glance at your TODO file should get you back to speed quickly.&lt;/p>
&lt;p>When you are done with your task, select the line in your TODO file and hit Alt+D (or Toggle Done from context menu). I would also recommend commiting all the changes at this point, including the changes to your TODO file. Personally, I like to call these &amp;ldquo;work in progress&amp;rdquo; commits, or &amp;ldquo;wip:&amp;rdquo; to follow conventional commits.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Mark task as done" srcset="
/blog/2024/05/managing-context-switches/03-task-done-dark_hu5d7c1d516dbab999232aa4a0f0279da7_32879_0d75063139160f5e1e36d5df36d733ed.webp 400w,
/blog/2024/05/managing-context-switches/03-task-done-dark_hu5d7c1d516dbab999232aa4a0f0279da7_32879_764e7f6e847fe9e1aae1aadde02bfde4.webp 760w,
/blog/2024/05/managing-context-switches/03-task-done-dark_hu5d7c1d516dbab999232aa4a0f0279da7_32879_1200x1200_fit_q95_h2_lanczos_3.webp 1200w"
src="https://msound.net/blog/2024/05/managing-context-switches/03-task-done-dark_hu5d7c1d516dbab999232aa4a0f0279da7_32879_0d75063139160f5e1e36d5df36d733ed.webp"
width="468"
height="608"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;p>When all tasks are done, you can hit Ctrl+Shift+A to move all done tasks to the archived section in your TODO file. Again, be sure to commit the TODO file with your code changes for your main &lt;code>feat:&lt;/code> or &lt;code>fix:&lt;/code> commit at this point.&lt;/p>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-100" >&lt;img alt="Archive completed tasks" srcset="
/blog/2024/05/managing-context-switches/04-archived-dark_hu970982ba9866d68ea0f97ea130f31d55_52566_fa88b60c1bcbf14fa0e517f7ceed9b8a.webp 400w,
/blog/2024/05/managing-context-switches/04-archived-dark_hu970982ba9866d68ea0f97ea130f31d55_52566_d5ff47886aa4ea42d2185fa04e6b141e.webp 760w,
/blog/2024/05/managing-context-switches/04-archived-dark_hu970982ba9866d68ea0f97ea130f31d55_52566_1200x1200_fit_q95_h2_lanczos_3.webp 1200w"
src="https://msound.net/blog/2024/05/managing-context-switches/04-archived-dark_hu970982ba9866d68ea0f97ea130f31d55_52566_fa88b60c1bcbf14fa0e517f7ceed9b8a.webp"
width="468"
height="608"
loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;h2 id="easy-solution">Easy Solution&lt;/h2>
&lt;p>The easy solution to all this would be to reduce interruptions in the first place. But it&amp;rsquo;s easier said than done. Some companies are adopting a &amp;ldquo;no meeting Thursdays&amp;rdquo; policy. Is your team suffering under a lot of context switches? What have you tried, and what has worked / not worked for you?&lt;/p></description></item><item><title>Architecture Decision Records</title><link>https://msound.net/blog/2024/02/architecture-decision-records/</link><pubDate>Sat, 24 Feb 2024 00:00:00 +0000</pubDate><guid>https://msound.net/blog/2024/02/architecture-decision-records/</guid><description>&lt;p>Software Development is part science and part art. Not unlike making a movie. But a software developer&amp;rsquo;s role is very different from a movie director&amp;rsquo;s in one important way: When a director makes the final cut and releases the movie to the audience, the movie is in its finished state. The director will have to live with whatever goofs slipped through during post production. A software, on the other hand, is never done. If a bug slipped through in this version, you can always fix it in the next version.&lt;/p>
&lt;p>Which brings us to an interesting point: The developer that started working on a software may or may not be the same person that releases it into the wild. A lot of software projects go through a hand-off process, sometimes even multiple times. And anyone who has ever seen children play telephone has seen that every hand-off diminishes the quality of the message. This leads to a situation (more often than not) of a software team wondering why the original developers decided to do things in a certain way.&lt;/p>
&lt;p>Enter Architecture Decision Records.&lt;/p>
&lt;p>The goal of &lt;strong>Architecture Decision Records&lt;/strong> (ADRs) is to capture the thinking process that went into making significant decisions regarding the architecture of the software. The idea was made popular by &lt;a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions" target="_blank" rel="noopener">Michael Nygard&amp;rsquo;s blog post&lt;/a>.&lt;/p>
&lt;p>ADRs can be used to capture a technical problem, the various options considered, pros/cons of each option, and the final decision made along with the the reasoning.&lt;/p>
&lt;p>It goes without saying that the developers making the ADR should be open and honest about their thought process - just like it is important for future teams reading the ADRs not to judge prior team&amp;rsquo;s decisions - but to understand the context and assumptions under which the decisions were made - and to be thankful for the prior team&amp;rsquo;s efforts in recording the ADRs.&lt;/p>
&lt;p>Over the last few years, several templates have been proposed for capturing ADRs.&lt;/p>
&lt;p>The one that I have settled is on called &lt;a href="https://adr.github.io/madr/" target="_blank" rel="noopener">Markdown Any Decision Record&lt;/a> or MADR for short.&lt;/p>
&lt;p>MADR uses markdown syntax - something most developers are already be aware of. MADR has simple and easy to understand sections to capture the problem statement, decision drivers, various options considered, final decision - its consequences and so on.&lt;/p>
&lt;p>MADR also recommends storing the ADRs alongside your project files (in a folder called &lt;code>/docs/decisions&lt;/code>) and commit them to your git repo. Filenames are suggested to be of the format &lt;code>nnnn-title.md&lt;/code> where &lt;code>nnnn&lt;/code> is a running sequence of numbers starting from 0001. MADR dogfoods their own recommendation - so you can find their internal ADRs in their github repo &lt;a href="https://github.com/adr/madr/tree/develop/docs/decisions" target="_blank" rel="noopener">adr/madr&lt;/a>.&lt;/p>
&lt;p>The one thing you want to be careful about, is when you change a decision - do not edit an ADR - instead mark it as superseded and link it to a new ADR.&lt;/p>
&lt;p>If you are unsure about ADRs, just start using them. You can always refine your process. Remember, we are making software, not movies.&lt;/p></description></item><item><title>Versioning Go Applications</title><link>https://msound.net/blog/2023/11/versioning-go-apps/</link><pubDate>Sat, 18 Nov 2023 00:00:00 +0000</pubDate><guid>https://msound.net/blog/2023/11/versioning-go-apps/</guid><description>&lt;p>Inject version information in Go applications using linker flags.&lt;/p>
&lt;h2 id="overview">Overview&lt;/h2>
&lt;p>One of the must haves when it comes to running a Go application in production is to have a clear version number printed in the logs at the start of the application. This is helpful in the future when debugging a bug by looking at the logs, and seeing which version of the app caused this behavior.&lt;/p>
&lt;h2 id="get-version">Get Version&lt;/h2>
&lt;p>The version information can be stored as global variables with zero values, with the values being injected at build time by the linker.&lt;/p>
&lt;p>Here is an example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">package&lt;/span> &lt;span class="nx">main&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="s">&amp;#34;log&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">APP_VERSION&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">APP_PRERELEASE&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">APP_BUILD&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Hello world&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nf">getVersionString&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">getVersionString&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">version&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="s">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">APP_VERSION&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">version&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">APP_VERSION&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">version&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;1.0.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">APP_PRERELEASE&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">version&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">version&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s">&amp;#34;-&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">APP_PRERELEASE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">version&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">version&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s">&amp;#34;-local.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">APP_BUILD&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">&amp;gt;&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">version&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">version&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s">&amp;#34;+&amp;#34;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">APP_BUILD&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">version&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Notice that the variables &lt;code>APP_VERSION&lt;/code>, &lt;code>APP_PRERELEASE&lt;/code> and &lt;code>APP_BUILD&lt;/code> are declared at the global scope, but left at the zero value (&lt;code>&amp;quot;&amp;quot;&lt;/code>).&lt;/p>
&lt;p>If you run this as is, you will see the following output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ go run main.go
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2023/11/17 18:13:27 Hello world 1.0.0-local.1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="linker-flags">Linker flags&lt;/h2>
&lt;p>As the next step, we will be injecting these variables at build time using linker flags:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ go mod init github.com/msound/helloworld
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ go build -o helloworld -ldflags &amp;#34;-X &amp;#39;main.APP_VERSION=1.0.0&amp;#39; -X &amp;#39;main.APP_PRERELEASE=stage.a862776&amp;#39; -X &amp;#39;main.APP_BUILD=build.422&amp;#39;&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This would build a binary named &lt;code>helloworld&lt;/code> which would have the version information baked into the binary. Now if we run the binary:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">$ ./helloworld
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">2023/11/17 18:20:57 Hello world 1.0.0-stage.a862776+build.422
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="dockerfile">Dockerfile&lt;/h2>
&lt;p>You are probably going to run your Go app as a container, so you would want to pass these values through as arguments into a Dockerfile. Here&amp;rsquo;s how you would do it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span class="line">&lt;span class="cl">&lt;span class="n">FROM&lt;/span> &lt;span class="n">golang&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">1.21&lt;/span> &lt;span class="n">AS&lt;/span> &lt;span class="n">build&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">env&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ENV&lt;/span> &lt;span class="n">GOOS&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">linux&lt;/span> &lt;span class="n">GOARCH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">amd64&lt;/span> &lt;span class="n">CGO_ENABLED&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ARG&lt;/span> &lt;span class="n">APP_VERSION&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ARG&lt;/span> &lt;span class="n">APP_PRERELEASE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">ARG&lt;/span> &lt;span class="n">APP_BUILD&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">WORKDIR&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">app&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">COPY&lt;/span> &lt;span class="o">.&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">app&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">RUN&lt;/span> &lt;span class="n">go&lt;/span> &lt;span class="n">build&lt;/span> \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">-&lt;/span>&lt;span class="n">o&lt;/span> &lt;span class="n">helloworld&lt;/span> \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">-&lt;/span>&lt;span class="n">ldflags&lt;/span> &lt;span class="s2">&amp;#34;-X &amp;#39;main.APP_VERSION=${APP_VERSION}&amp;#39; -X &amp;#39;main.APP_PRERELEASE=${APP_PRERELEASE}&amp;#39; -X &amp;#39;main.APP_BUILD=${APP_BUILD}&amp;#39;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">###########################&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">FROM&lt;/span> &lt;span class="n">alpine&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mf">3.11&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">RUN&lt;/span> &lt;span class="n">apk&lt;/span> &lt;span class="n">update&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">apk&lt;/span> &lt;span class="n">add&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">no&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">cache&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">purge&lt;/span> &lt;span class="n">ca&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">certificates&lt;/span> &lt;span class="n">curl&lt;/span> &lt;span class="n">tzdata&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> \
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">rm&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">rf&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">cache&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">apk&lt;/span>&lt;span class="o">/*&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">tmp&lt;/span>&lt;span class="o">/*&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">RUN&lt;/span> &lt;span class="n">adduser&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="n">D&lt;/span> &lt;span class="n">appuser&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">uid&lt;/span> &lt;span class="mi">3000&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">COPY&lt;/span> &lt;span class="o">--&lt;/span>&lt;span class="n">from&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">build&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="n">env&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">helloworld&lt;/span> &lt;span class="o">/&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="n">helloworld&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">USER&lt;/span> &lt;span class="n">appuser&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">CMD&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;/app/helloworld&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The Dockerfile receives the version information as ARGs, and passes them on to &lt;code>go build&lt;/code> as linker arguments.&lt;/p>
&lt;h2 id="cicd-pipeline">CI/CD Pipeline&lt;/h2>
&lt;p>The final step is to pass the arguments to &lt;code>docker build&lt;/code> from the CI/CD Pipeline. Here is the snippet of this for Gitlab. For Github actions, you can adapt this as needed:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">script&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> export APP_VERSION=${CI_COMMIT_TAG}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> export APP_PRERELEASE=stage.${CI_COMMIT_SHORT_SHA}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> export APP_BUILD=build.${CI_JOB_ID}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> docker build \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --build-arg &amp;#34;APP_VERSION=${APP_VERSION}&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --build-arg &amp;#34;APP_PRERELEASE=${APP_PRERELEASE}&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --build-arg &amp;#34;APP_BUILD=${APP_BUILD}&amp;#34; \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> -t helloworld:${CI_COMMIT_TAG} .&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="references-and-credits">References and Credits&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://docs.docker.com/build/guide/build-args/" target="_blank" rel="noopener">https://docs.docker.com/build/guide/build-args/&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://www.digitalocean.com/community/tutorials/using-ldflags-to-set-version-information-for-go-applications" target="_blank" rel="noopener">https://www.digitalocean.com/community/tutorials/using-ldflags-to-set-version-information-for-go-applications&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Conventional Commits</title><link>https://msound.net/blog/2023/10/conventional-commits/</link><pubDate>Fri, 06 Oct 2023 00:00:00 +0000</pubDate><guid>https://msound.net/blog/2023/10/conventional-commits/</guid><description>&lt;p>Conventional Commits is a widely adpoted standard way of writing a commit message.&lt;/p>
&lt;h2 id="overview">Overview&lt;/h2>
&lt;p>Before proceeding further, please peruse the full spec from the &lt;a href="https://www.conventionalcommits.org/en/v1.0.0/" target="_blank" rel="noopener">Conventional Commits&lt;/a> offical website.&lt;/p>
&lt;p>The short summary of a conventional commit message is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">&amp;lt;type&amp;gt;[(scope)][!]: &amp;lt;description&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;lt;blank line&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[body]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&amp;lt;blank line&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">[footer(s)]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="types">Types&lt;/h2>
&lt;p>The standard recommends the use of the following types:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>build&lt;/strong>: Changes that affect the build system or external dependencies&lt;/li>
&lt;li>&lt;strong>chore&lt;/strong>: A code change that is neither a &lt;code>feat&lt;/code> or a &lt;code>fix&lt;/code> (example: bumping versions of dependencies)&lt;/li>
&lt;li>&lt;strong>ci&lt;/strong>: Changes to CI configuration files and scripts&lt;/li>
&lt;li>&lt;strong>docs&lt;/strong>: Documentation only changes&lt;/li>
&lt;li>&lt;strong>feat&lt;/strong>: A new feature&lt;/li>
&lt;li>&lt;strong>fix&lt;/strong>: A bug fix&lt;/li>
&lt;li>&lt;strong>perf&lt;/strong>: A code change that improves performance&lt;/li>
&lt;li>&lt;strong>refactor&lt;/strong>: A code change that neither fixes a bug nor adds a feature&lt;/li>
&lt;li>&lt;strong>revert&lt;/strong>: A commit that reverts a previous commit&lt;/li>
&lt;li>&lt;strong>style&lt;/strong>: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)&lt;/li>
&lt;li>&lt;strong>test&lt;/strong>: Adding missing tests or correcting existing tests&lt;/li>
&lt;li>&lt;strong>wip&lt;/strong>: Indicates this commit is a Work-In-Progress&lt;/li>
&lt;/ul>
&lt;h2 id="scopes">Scopes&lt;/h2>
&lt;p>Every repo should have a list of scopes clearly defined in the repo&amp;rsquo;s README file. Commit message may use a scope to clarify what part of the codebase this commit touches. For multiple scopes, use a comma separation.&lt;/p>
&lt;h2 id="breaking-change">Breaking Change&lt;/h2>
&lt;p>If the commit introduces a breaking change, it must be indicated using an exclamation (!) after the type/scope and before the colon (:).&lt;/p>
&lt;h2 id="description">Description&lt;/h2>
&lt;p>The description is a short summary of the code changes. The description shall be written in imperative. The first line, including type, scope and description shall not exceed 50 characters. TIP: If you set your git config &lt;code>core.editor=vim&lt;/code>, it will automatically highlight when you are going over 50 characters. Description shall start with a lower case.&lt;/p>
&lt;p>Here are some correct and wrong examples for description:&lt;/p>
&lt;p>✅ add foo to main menu&lt;/p>
&lt;p>❌ adding foo to main menu&lt;/p>
&lt;p>❌ added foo to main menu&lt;/p>
&lt;p>A good way to think about writing in imperative, is to try to fill in the blank of the sentence:&lt;/p>
&lt;p>If merged, this commit will _________________________.&lt;/p>
&lt;p>What you write in the blank will usually be in imperative.&lt;/p>
&lt;h2 id="body">Body&lt;/h2>
&lt;p>A commit body is free-form and may consist of any number of newline separated paragraphs. Try to add more context about the commit focussing on &lt;strong>WHY&lt;/strong> this change was introduced rather than &lt;strong>WHAT&lt;/strong> code was changed. Most developers can see the commit diff and easily figure out the WHAT part - answering the WHY question adds value to the commit message. Remember, you yourself may be wondering about WHY you made this change on your own commit a few months down the road.&lt;/p>
&lt;h2 id="footer">Footer&lt;/h2>
&lt;p>One or more footers may be provided one blank line after the body. Each footer must consist of a word token, followed by a :&lt;space> separator, followed by a string value. The token must not have spaces, instead it must use hyphen(&lt;code>-&lt;/code>) to separate words.&lt;/p>
&lt;p>This is an non-exhaustive example list of possible footers:&lt;/p>
&lt;p>BREAKING-CHANGE: This commit will break the code for database records in the old schema.&lt;/p>
&lt;p>Closes: JIRA-123&lt;/p>
&lt;p>Refer: JIRA-456&lt;/p>
&lt;p>Reviewed-by: Bob &lt;a href="mailto:bob@example.com">bob@example.com&lt;/a>&lt;/p>
&lt;p>Signed-off-by: Alice &lt;a href="mailto:alice@example.com">alice@example.com&lt;/a>&lt;/p>
&lt;h2 id="why">Why&lt;/h2>
&lt;p>Conventional commits standardize and make it easy to read commit messages. It also helps in writing tools to parse commit messages are automatically generate changelogs, or suggest semantic version number updates based on the presence of &lt;code>fix&lt;/code> (patch version is incremented) or &lt;code>feat&lt;/code> (minor version is incremented) etc. Tools can also parse the &lt;code>token: value&lt;/code> syntax of the footers to process information contained therein.&lt;/p>
&lt;h2 id="references-and-credits">References and Credits&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.conventionalcommits.org/en/v1.0.0/" target="_blank" rel="noopener">https://www.conventionalcommits.org/en/v1.0.0/&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines" target="_blank" rel="noopener">https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional" target="_blank" rel="noopener">https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/conventional-changelog/conventional-changelog" target="_blank" rel="noopener">https://github.com/conventional-changelog/conventional-changelog&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Managing secrets using sops/age</title><link>https://msound.net/blog/2023/05/managing-secrets/</link><pubDate>Fri, 26 May 2023 00:00:00 +0000</pubDate><guid>https://msound.net/blog/2023/05/managing-secrets/</guid><description>&lt;p>Store your secrets right in your git repo using sops / age for encryption and decryption.&lt;/p>
&lt;div class="flex px-4 py-3 rounded-md bg-yellow-100 dark:bg-yellow-900">
&lt;span class="pr-3 pt-1 text-red-400">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0zM12 15.75h.007v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">DO NOT commit unencrypted secrets to Git.&lt;/span>
&lt;/div>
&lt;h2 id="toolchain">Toolchain&lt;/h2>
&lt;p>We use &lt;code>sops&lt;/code> and &lt;code>age&lt;/code> to handle encryption of secrets.&lt;/p>
&lt;p>Sops can be downloaded from &lt;a href="https://github.com/getsops/sops/releases" target="_blank" rel="noopener">https://github.com/getsops/sops/releases&lt;/a>&lt;/p>
&lt;p>Age can be installed from &lt;a href="https://github.com/FiloSottile/age#installation" target="_blank" rel="noopener">https://github.com/FiloSottile/age#installation&lt;/a>&lt;/p>
&lt;div class="flex px-4 py-3 rounded-md bg-primary-100 dark:bg-primary-900">
&lt;span class="pr-3 pt-1 text-primary-400">
&lt;svg height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">&lt;path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.25 11.25l.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0a9 9 0 0 1 18 0m-9-3.75h.008v.008H12z"/>&lt;/svg>
&lt;/span>
&lt;span class="dark:text-neutral-300">Age is pronounced with a hard G. Like Aghey.&lt;/span>
&lt;/div>
&lt;h2 id="files">Files&lt;/h2>
&lt;p>Secrets must be stored in text files, such as YAML or ENV files. In case the secret is managed via Kubernetes, the YAML is typically a manifest file representing a Secret. In case of a frontend application, the secrets are typically stored in a .env file format.&lt;/p>
&lt;p>There should be a clear distinction between encrypted and unencrypted secret files. To make this distinction, all encrypted files should have the suffix &lt;code>.enc.yaml&lt;/code> or &lt;code>.enc.env&lt;/code>, where &lt;code>.enc&lt;/code> signifies that the file is encrypted. Example: &lt;code>secrets.enc.yaml&lt;/code> or &lt;code>stage.enc.env&lt;/code>&lt;/p>
&lt;p>To be very safe, is it better to add the plain names of the files to &lt;code>.gitignore&lt;/code> so that no one accidentally commits an unencrypted file to git.&lt;/p>
&lt;h2 id="keys">Keys&lt;/h2>
&lt;p>Before we begin to encrypt secrets, we need to create a keypair using &lt;code>age&lt;/code>. To do this, run:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">age-keygen
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The output will look like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># created: 2023-05-08T15:38:39-04:00&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># public key: age1xxxx&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">AGE-SECRET-KEY-yyyy
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The public key, also known as identity, or recipient, looks like &lt;code>age1xxxx&lt;/code>.&lt;/p>
&lt;p>The secret key, or simply key, looks like &lt;code>AGE-SECRET-KEY-yyyy&lt;/code>.&lt;/p>
&lt;p>Store the output of the command (including comments) in a file &lt;code>/home/yourname/.config/sops/age/keys.txt&lt;/code>(for Linux), or &lt;code>/Users/yourname/Library/Application Support/sops/age/keys.txt&lt;/code> (for Mac)&lt;/p>
&lt;p>If you use Bitwarden, it is a good idea to backup this in your vault. It is important you do this before moving forward. If you lose the key, it is NOT possible to recover it. (However, if you lose the public key, you can derive it from the secret key using &lt;code>age-keygen -y&lt;/code>)&lt;/p>
&lt;h2 id="encrpytion">Encrpytion&lt;/h2>
&lt;p>Now, we will use sops to encrypt the secret file.&lt;/p>
&lt;p>In case of YAML file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sops -e -a age1xxxx secrets.yaml &amp;gt; secrets.enc.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In case of ENV file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sops -e -a age1xxxx stage.env &amp;gt; stage.enc.env
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Always review the file before committing to git, making sure that the file contents are indeed encrypted.&lt;/p>
&lt;h2 id="decryption">Decryption&lt;/h2>
&lt;p>In your local laptop, if you need a edit a secret, you can use &lt;code>sops&lt;/code> to directly edit the file in an editor like &lt;code>vi&lt;/code>. Run the command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sops secrets.enc.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In your local laptop, if you want to dump the secret to your terminal to review, you can run:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sops -d secrets.enc.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In order for this to work, you should have the key stored in your &lt;code>keys.txt&lt;/code> file as described in earlier section.&lt;/p>
&lt;h2 id="configuration">Configuration&lt;/h2>
&lt;p>Consider a Kubernetes secret manifest that looks like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">myapp-secrets&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">myapp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">AUTHORIZATION_USERNAME&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">edward&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">AUTHORIZATION_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">MargaretThatcherIs110%Sexy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Encrypt it using the command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sops -e age19esgdwuqmu75sl9mrkxrr9sn2yla82jtfzhf3ua2430r3ftttgxqrglsyv secrets.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And you will get this result:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ENC[AES256_GCM,data:Efc=,iv:SF0ZIFF+dVm//VweksahH/MIbJajj2TX82jwEooLFG0=,tag:ZJ36YUfz/4YEhaNJmkYZog==,type:str]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ENC[AES256_GCM,data:arfGKcp4,iv:7OzQL9pCqyLTfib3t9OOfldGGxG0igAJi43Gx3q5okY=,tag:9Pjr6GsVMfIYxZ1oZIO2Xw==,type:str]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ENC[AES256_GCM,data:ErHyEow3CpuTtoq8tQ==,iv:11MFZgAHuPRcGDL0EWntDT14vM2ix8VKwOy914jXKB8=,tag:SkGF+LTUb4rRaHc1NlUVbg==,type:str]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ENC[AES256_GCM,data:8YUTU0U=,iv:hAExMObv3gSXhojEhYcW3hEePEgNVGPkeeJzJIYjetY=,tag:Ez2A4yhXiRj3cMzuNDddzw==,type:str]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">AUTHORIZATION_USERNAME&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ENC[AES256_GCM,data:PWRGPGHk,iv:P6fXYtS+6ETypBOEcgoNjxakcoYOjwcxqStZDJEU7pM=,tag:a5anLYBaKGjsL8dm3o1ovA==,type:str]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">AUTHORIZATION_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ENC[AES256_GCM,data:3BMUm60XEqoOfHQyif7zTm/s5ZMOnRtoIj4=,iv:DwP5UCDjYONqE7ShI9wM98KD+qo5/YFp01/f/GQznhc=,tag:vJvGS7Rym058/VzODtR+ig==,type:str]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">sops&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kms&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gcp_kms&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">azure_kv&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hc_vault&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">age&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">recipient&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">age19esgdwuqmu75sl9mrkxrr9sn2yla82jtfzhf3ua2430r3ftttgxqrglsyv&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enc&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> -----BEGIN AGE ENCRYPTED FILE-----
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlMTh0aFRaQ2JjSWtjNHFt
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> Q0VLRmJoVm0vMjBTdThSTFRjT3dlWDZDR3lrCjllZ2l5eUJ3bC9iZUEyRU1LSWdk
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> OU9ZdTM1K0NHT09LK2YxTjNoNTRDRjAKLS0tIHA5UWh3cVlHNGpWWXBqUm11UFNi
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ZWZsb2RQb2NyY1h4YzZNOE1tempvc2sKgZX1kiUuImKMJa0bTdoWLmiLIrABTDHf
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> ZLOi2RUTOh/Ui+mfV1dcYrUV6SKJvlGwwRZEeuuBrG6Jbd5W32A6OQ==
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> -----END AGE ENCRYPTED FILE-----&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lastmodified&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;2024-05-21T19:42:28Z&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mac&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ENC[AES256_GCM,data:sHZMYFmP6/HvLGz0LqMQ2kWVvvWVS+K01IoaqeXSkyE3nR9Qi5JJJ6aeRMjfhDygBMGw2P+0zS/6KcPBoeQpvKWbWZFAyWcTZJIx+iI8bIsWivtMOlNydE61C+0xewVIvej3AyKkSyxdSbuDO/1/rLg9KXjkJJ58TtYxz+jk8MA=,iv:eM9viOEs61XQ0f4mkgoHsAi9bxtRUHSQ1IgiPIslAW4=,tag:1iKWK7FlKpzFBH/6wr3AJg==,type:str]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pgp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">unencrypted_suffix&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">_unencrypted&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3.8.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Notice that all keys in the yaml including non-sensitive ones like apiVersion, kind and metadata are also encrypted. This may be underisarable when doing code reviews or otherwise looking at the encrypted file.&lt;/p>
&lt;p>To avoid this, create a .sops.yaml (this file is usually placed in the root folder of your repo):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">creation_rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">path_regex&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">secrets.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">age&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">age19esgdwuqmu75sl9mrkxrr9sn2yla82jtfzhf3ua2430r3ftttgxqrglsyv&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">encrypted_regex&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">^(data|stringData)$&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now, you can encrypt without specifying any identities:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">sops -e secrets.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And you will get this response:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">myapp-secrets&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">myapp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">stringData&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">AUTHORIZATION_USERNAME&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ENC[AES256_GCM,data:Lyj81idG,iv:w77ofABHiYHyup8z/bUMqFbnZ1oL82H1gbgQ2m2ixRQ=,tag:8IIQi4hWgiGRfRP6jYWxRw==,type:str]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">AUTHORIZATION_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ENC[AES256_GCM,data:8Bgfii+UiFTGURyFSvJZ4RZuzRVgMG33RFM=,iv:ToBjzFzrGCbpauXYfa3PNDuxTGerUfmmtNovWah+rNs=,tag:hkTNGymGTtPo6O5q823LQw==,type:str]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">sops&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">kms&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gcp_kms&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">azure_kv&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hc_vault&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">age&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">recipient&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">age19esgdwuqmu75sl9mrkxrr9sn2yla82jtfzhf3ua2430r3ftttgxqrglsyv&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">enc&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> -----BEGIN AGE ENCRYPTED FILE-----
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCN2toTmFFMS9FdVduNFBU
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> eWRKWmcvSnE3Wk8wVzlUZTN4QVdiRUlZSzFZCnlWNytrZTBJcjMzcklWVnFNb3k3
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> YjRCdE52dDhiYk1WdHkxNXoxZlZraDQKLS0tIGVCNFdIV0ZuREtjU3NlM0dCcVhn
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> QUtwN3BmUGNwYldabTcyQ1dvc1FYTkkKvl41B3MxZVER+BHBGiQJPQW62cm8TsiK
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> TIYIqVcJ4HpKdCe6t2KFVnfDHwVJ1BbarUOMGWjMfwaLMpOlB6fRgQ==
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> -----END AGE ENCRYPTED FILE-----&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">lastmodified&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;2024-05-21T19:49:05Z&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">mac&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ENC[AES256_GCM,data:aJAVVBeh0u8o02QH+hnBfewYnVlt+omOQY+IXF7FixWDNtNTRBwMb0JbqAt25rda3P7zELgRd1bfDnClFY7JadolIFN1iwPi8w/hQSv5Vp32N259ukpzxpWozT7Xl7CkR1XaOMvXTTQw2Uv4RfdyyrTxIlPAwsjCfShbiVjFf2k=,iv:niizOhZ9TeEWr1Vms+h/F6wubQZND197q6XpAMMqa7k=,tag:XxAsF1n8nQFtNepf7ePwsg==,type:str]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pgp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">encrypted_regex&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">^(data|stringData)$&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">3.8.1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>As you will note, only the keys under &lt;code>stringData&lt;/code> are encrypted.&lt;/p>
&lt;h2 id="cicd-pipelines">CI/CD pipelines&lt;/h2>
&lt;p>For decrypting secrets in CI/CD pipeline, run:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">SOPS_AGE_KEY&lt;/span>&lt;span class="o">=&lt;/span>AGE-SECRET-KEY-yyyy
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">sops -d secrets.enc.yaml &amp;gt; secrets.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You can also store the secret key in a Github actions env var and use that here. And finally, you can use the decrypted secrets.yaml in a &lt;code>kubectl apply -f&lt;/code> command.&lt;/p></description></item></channel></rss>