<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Artur Bednarczyk | Isur's blog]]></title><description><![CDATA[Artur Bednarczyk | Isur's blog]]></description><link>https://blog.isur.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 09 May 2026 13:49:53 GMT</lastBuildDate><atom:link href="https://blog.isur.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Dotfiles with Ansible]]></title><description><![CDATA[TL:DR
How to quickly configure a fresh system. A quick introduction to Ansible and automation of personal setup for macOS and Arch Linux. Installation of apps, config files, and secrets.
The problem
Setting up a fresh system might be a little cumbers...]]></description><link>https://blog.isur.dev/dotfiles-with-ansible</link><guid isPermaLink="true">https://blog.isur.dev/dotfiles-with-ansible</guid><category><![CDATA[Programming Blogs]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[ansible]]></category><category><![CDATA[automation]]></category><category><![CDATA[Linux]]></category><category><![CDATA[macOS]]></category><category><![CDATA[dotfiles]]></category><dc:creator><![CDATA[Artur Bednarczyk]]></dc:creator><pubDate>Mon, 18 Nov 2024 20:05:33 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-tldr">TL:DR</h1>
<p>How to quickly configure a fresh system. A quick introduction to Ansible and automation of personal setup for macOS and Arch Linux. Installation of apps, config files, and secrets.</p>
<h1 id="heading-the-problem">The problem</h1>
<p>Setting up a fresh system might be a little cumbersome. You already have your perfect setup. Everything is installed, everything is configured.</p>
<p>Whether you got a new computer, are reinstalling due to an upgrade, or are just setting up a virtual machine, it is time-consuming to set up everything just as you like.</p>
<p>It is not only time-consuming, but it is also easy to forget some things or just forget how to configure something.</p>
<p>How can we solve that problem?</p>
<h2 id="heading-solutions">Solutions</h2>
<p>There are a few ways to handle this situation.</p>
<h3 id="heading-dotfiles">Dotfiles</h3>
<p>The simplest way is to copy config files and store them somewhere, for example, in a git repository. A lot of people store their <code>dotfiles</code> in a repository. We can make some README or other notes to remember how to set it up as we like.</p>
<p>This was also my first approach to this. But wouldn't it be better to automate it?</p>
<h3 id="heading-bash">Bash</h3>
<p>I've created a lot of bash scripts to set up everything as I wanted, and it was working just fine. But it was not so good at maintenance.</p>
<p>So, looking for something better, I gave Ansible a try.</p>
<h3 id="heading-ansible">Ansible</h3>
<p>This is simple to use, easy to maintain, and gives a lot of possibilities to automate virtually anything. So what it is and how to use it?</p>
<h1 id="heading-introduction-to-ansible">Introduction to ansible</h1>
<h2 id="heading-what-is-it">What is it?</h2>
<p>Ansible is software that enables automation and orchestration. It can automate virtually any task.</p>
<blockquote>
<p>Ansible is an open source, command-line IT automation software application written in Python. It can configure systems, deploy software, and orchestrate advanced workflows to support application deployment, system updates, and more. ~ <a target="_blank" href="https://www.ansible.com/how-ansible-works/">ansible.com</a></p>
</blockquote>
<p>It is designed around the following principles:</p>
<ul>
<li><p>agent-less architecture - low maintenance;</p>
</li>
<li><p>simplicity - simple <code>YAML</code> syntax, using <code>SSH</code> to connect to the machines;</p>
</li>
<li><p>scalability and flexibility - easily and quickly scale through modular design;</p>
</li>
<li><p>idempotence and predictability - when the system is in the desired state, it will not change even if the playbook is run multiple times;</p>
</li>
</ul>
<h2 id="heading-how-to-install">How to install</h2>
<p>Ansible can be installed using Python and <code>pip</code>. Or, for some systems like Arch Linux and macOS, we may use package managers.</p>
<p>For Arch Linux using <code>yay</code>:</p>
<pre><code class="lang-bash">yay -S ansible
</code></pre>
<p>For MacOS using <code>brew</code>:</p>
<pre><code class="lang-bash">brew install ansible
</code></pre>
<p>For more detailed information about installation look here: <a target="_blank" href="https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html">documentation</a>.</p>
<h2 id="heading-basics">Basics</h2>
<h3 id="heading-inventory">Inventory</h3>
<p>Ansible needs to know which machines it will manage. To achieve that, all we need is a simple inventory file in <code>ini</code> or <code>yaml</code> format.</p>
<pre><code class="lang-ini"><span class="hljs-section">[webservers]</span>
192.168.1.11
192.168.1.12

<span class="hljs-section">[databases]</span>
192.168.1.21
192.168.1.22
</code></pre>
<p>or</p>
<pre><code class="lang-yaml"><span class="hljs-attr">webservers:</span>
  <span class="hljs-attr">hosts:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-number">192.168</span><span class="hljs-number">.1</span><span class="hljs-number">.11</span>
    <span class="hljs-attr">app2:</span> <span class="hljs-number">192.168</span><span class="hljs-number">.1</span><span class="hljs-number">.12</span>
<span class="hljs-attr">databases:</span>
  <span class="hljs-attr">hosts:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-number">192.168</span><span class="hljs-number">.1</span><span class="hljs-number">.21</span>
    <span class="hljs-attr">app2:</span> <span class="hljs-number">192.168</span><span class="hljs-number">.1</span><span class="hljs-number">.22</span>
</code></pre>
<p>Inventory defines the managed nodes and groups of them. In the above example, we have two groups - <code>webservers</code> and <code>databases</code>. Each group has two IP addresses. By default, Ansible also creates two other groups - <code>all</code> and <code>ungrouped</code>. The first one contains all hosts, and the second one includes all hosts not grouped under anything other than <code>all</code>.</p>
<p>There are some more things that we can set in the inventory, like the user that we are using:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">webservers:</span>
  <span class="hljs-attr">hosts:</span>
    <span class="hljs-attr">app:</span>
      <span class="hljs-attr">ansible_host:</span> <span class="hljs-number">192.168</span><span class="hljs-number">.1</span><span class="hljs-number">.11</span>
      <span class="hljs-attr">ansible_user:</span> <span class="hljs-string">app_user</span>
</code></pre>
<p>There is more, and we can read about this in the official <a target="_blank" href="https://docs.ansible.com/ansible/latest/inventory_guide/index.html">documentation</a>.</p>
<h3 id="heading-task">Task</h3>
<p>A task is a single automation, like installing a package or creating a directory.</p>
<h4 id="heading-create-task">Create task</h4>
<p>A task can be created in a separate <code>yaml</code> file or directly in a playbook.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Hello</span> <span class="hljs-string">world</span> <span class="hljs-string">task</span>
  <span class="hljs-attr">ansible.builtin.debug:</span>
    <span class="hljs-attr">msg:</span> <span class="hljs-string">"Hello world!"</span>
</code></pre>
<h3 id="heading-modules">Modules</h3>
<p>Modules or plugins are units that are run from the command line or tasks. Ansible executes each module, usually on a managed node (host), and collects the return value from it. Modules can be collected into collections. We can use built-in modules, modules from the community, or create our own.</p>
<p>Modules can be called in tasks, as in the above task example.</p>
<p>To use the command line:</p>
<pre><code class="lang-bash">ansible home -m debug -a <span class="hljs-string">"msg=Hello!"</span> -i inventory.yml
</code></pre>
<p>home is a group of hosts from the inventory that we specified with <code>-i inventory.yml</code></p>
<p>This should result in:</p>
<pre><code class="lang-bash">pc | SUCCESS =&gt; {
    <span class="hljs-string">"msg"</span>: <span class="hljs-string">"Hello!"</span>
}
</code></pre>
<p>pc is a host from the inventory.</p>
<h3 id="heading-collection">Collection</h3>
<p>Collections are a distribution format for Ansible content that can include playbooks, roles, modules, and plugins. They are useful when using something that is not built-in, created by someone else.</p>
<p>To install collections, we can use <code>ansible-galaxy</code> and a requirements file.</p>
<p>Let's say that we need modules to install packages from <code>aur</code> and <code>brew</code>. To do this, we need <code>community.general</code> and <code>kewlfft.aur</code>.</p>
<p>With a <code>requirements.yml</code> file:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">collections:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">community.general</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">kewlfft.aur</span>
</code></pre>
<p>We can use galaxy:</p>
<pre><code class="lang-bash">ansible-galaxy install -r requirements.yml
</code></pre>
<p>It will install all collections from the file, and they will be available for use.</p>
<h3 id="heading-role">Role</h3>
<p>A structured way to organize playbooks into reusable components. It contains tasks, variables, files, templates, and more.</p>
<p>Roles go into the <code>roles</code> directory and have a specified structure. Below we have an example from <a target="_blank" href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html">documentation</a>:</p>
<pre><code class="lang-bash">roles/
    common/               <span class="hljs-comment"># this hierarchy represents a "role"</span>
        tasks/            <span class="hljs-comment">#</span>
            main.yml      <span class="hljs-comment">#  &lt;-- tasks file can include smaller files if warranted</span>
        handlers/         <span class="hljs-comment">#</span>
            main.yml      <span class="hljs-comment">#  &lt;-- handlers file</span>
        templates/        <span class="hljs-comment">#  &lt;-- files for use with the template resource</span>
            ntp.conf.j2   <span class="hljs-comment">#  &lt;------- templates end in .j2</span>
        files/            <span class="hljs-comment">#</span>
            bar.txt       <span class="hljs-comment">#  &lt;-- files for use with the copy resource</span>
            foo.sh        <span class="hljs-comment">#  &lt;-- script files for use with the script resource</span>
        vars/             <span class="hljs-comment">#</span>
            main.yml      <span class="hljs-comment">#  &lt;-- variables associated with this role</span>
        defaults/         <span class="hljs-comment">#</span>
            main.yml      <span class="hljs-comment">#  &lt;-- default lower priority variables for this role</span>
        meta/             <span class="hljs-comment">#</span>
            main.yml      <span class="hljs-comment">#  &lt;-- role dependencies</span>
        library/          <span class="hljs-comment"># roles can also include custom modules</span>
        module_utils/     <span class="hljs-comment"># roles can also include custom module_utils</span>
        lookup_plugins/   <span class="hljs-comment"># or other types of plugins, like lookup in this case</span>
</code></pre>
<p>By default, when using a role, Ansible will look for a <code>main.yml</code> (or <code>main.yaml</code>, or <code>main</code>) file.</p>
<p>However, it is still possible to have multiple files there and organize everything according to needs.</p>
<h3 id="heading-play">Play</h3>
<p>This is a section within a playbook that defines what to run and on which host group.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Simple</span> <span class="hljs-string">play</span>
  <span class="hljs-attr">hosts:</span> <span class="hljs-string">webservers</span>
  <span class="hljs-attr">roles:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">common</span>
  <span class="hljs-attr">tasks:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Hello</span>
      <span class="hljs-attr">ansible.builtin.debug:</span>
        <span class="hljs-attr">msg:</span> <span class="hljs-string">Hello!</span>
</code></pre>
<p>This is one play that will run the role <code>common</code> and one task.</p>
<h3 id="heading-playbook">Playbook</h3>
<p>A playbook is a blueprint for automation. It is a simple configuration for what to run on nodes specified in the inventory.</p>
<p>Each playbook is built from plays that are run from top to bottom.</p>
<h3 id="heading-facts">Facts</h3>
<p>Data related to the remote system Ansible manages is available as facts. To access those variables, use <code>ansible_facts</code> or <code>ansible_{name}</code>.</p>
<h2 id="heading-how-to-use">How to use</h2>
<p>Let's build a simple example that can be run locally.</p>
<h3 id="heading-inventory-1">Inventory</h3>
<p>To achieve this, we need to set up the inventory correctly.</p>
<p>Create <code>inventory.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">home:</span>
  <span class="hljs-attr">hosts:</span>
    <span class="hljs-attr">pc:</span>
      <span class="hljs-attr">ansible_host:</span> <span class="hljs-string">localhost</span>
      <span class="hljs-attr">ansible_user:</span> <span class="hljs-string">isur</span>
      <span class="hljs-attr">ansible_connection:</span> <span class="hljs-string">local</span>
</code></pre>
<p>I have created a group <code>home</code> with the <code>pc</code> host that has some settings:</p>
<ul>
<li><p><code>ansible_host</code> - host - we want to use it locally, so <code>localhost</code></p>
</li>
<li><p><code>ansible_user</code>- this is the user that will be used on a machine - set it for your user</p>
</li>
<li><p><code>ansible_connection: local</code> - we do not want to use an SSH connection to our own system</p>
</li>
</ul>
<h3 id="heading-running-playbook">Running playbook</h3>
<p>To run a playbook, use the command <code>ansible-playbook</code> with the path to the playbook as an argument. To select the inventory, use <code>-i</code>:</p>
<pre><code class="lang-bash">ansible-playbook play.yml -i inventory.yml
</code></pre>
<p>Before running this command, first create a playbook.</p>
<h3 id="heading-simple-playbook">Simple playbook</h3>
<p>Create <code>playbook.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Simple</span> <span class="hljs-string">play</span>
  <span class="hljs-attr">hosts:</span> <span class="hljs-string">home</span>
  <span class="hljs-attr">tasks:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Hello</span> <span class="hljs-string">user</span>
      <span class="hljs-attr">ansible.builtin.debug:</span>
        <span class="hljs-attr">msg:</span> <span class="hljs-string">"Hello <span class="hljs-template-variable">{{ ansible_user }}</span>!"</span>
</code></pre>
<p>Ansible has a built-in debug module that lets you print out messages. We can use variables with templating like above. <code>ansible_user</code> is a fact with information about the username.</p>
<p>Below you can see the result of that task.</p>
<pre><code class="lang-bash">TASK [Hello user] *******************************************************************
ok: [pc] =&gt; {
    <span class="hljs-string">"msg"</span>: <span class="hljs-string">"Hello isur!"</span>
}
</code></pre>
<p>That might be all we need for a simple playbook. But when we have more tasks it might be better to move them to files and import them later.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Simple</span> <span class="hljs-string">task</span> <span class="hljs-string">imported</span>
  <span class="hljs-attr">hosts:</span> <span class="hljs-string">home</span>
  <span class="hljs-attr">tasks:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">ansible.builtin.import_tasks:</span> <span class="hljs-string">./tasks/hello.yml</span>
</code></pre>
<p>That way, we can group some tasks or even import tasks into other tasks.</p>
<p>But there is an even better way to organize automation.</p>
<h3 id="heading-with-roles">With roles</h3>
<p>Instead of grouping tasks into random directories, we can use <code>roles</code>.</p>
<p>Let's move the hello task into a role, and now the file structure should look like this:</p>
<pre><code class="lang-bash">./
    inventory.yml
    play.yml
    roles/
        hello/
            tasks/
                main.yml
</code></pre>
<p>And instead of importing tasks, we can use roles:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Play</span> <span class="hljs-string">with</span> <span class="hljs-string">roles</span>
  <span class="hljs-attr">hosts:</span> <span class="hljs-string">home</span>
  <span class="hljs-attr">roles:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">hello</span>
</code></pre>
<p><code>main.yml</code> in the hello role will look the same as any other task, but it will be easier to maintain and extend. Now we can add something more to it, so we will have some initial information when running the playbook.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Hello</span> <span class="hljs-string">user</span>
  <span class="hljs-attr">ansible.builtin.debug:</span>
    <span class="hljs-attr">msg:</span> <span class="hljs-string">"Hello <span class="hljs-template-variable">{{ ansible_user }}</span>!"</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Hello</span> <span class="hljs-string">system</span>
  <span class="hljs-attr">ansible.builtin.debug:</span>
    <span class="hljs-attr">msg:</span> <span class="hljs-string">"System: <span class="hljs-template-variable">{{ ansible_os_family }}</span>"</span>
</code></pre>
<p>Now, after running this, we can see which user and what system is used:</p>
<pre><code class="lang-bash">TASK [hello : Hello user] ******************************************************
ok: [pc] =&gt; {
    <span class="hljs-string">"msg"</span>: <span class="hljs-string">"Hello isur!"</span>
}

TASK [hello : Hello system] ****************************************************
ok: [pc] =&gt; {
    <span class="hljs-string">"msg"</span>: <span class="hljs-string">"System: Archlinux"</span>
}
</code></pre>
<p>Those facts <code>ansible_user</code> and <code>ansible_os_family</code> might be useful when we want to do something depending on the operating system or username. We might also use environment variables with <code>ansible_env</code>.</p>
<h1 id="heading-personal-system-setup">Personal system setup</h1>
<h2 id="heading-goal">Goal</h2>
<p>The goal is to prepare automation to handle two systems - Arch Linux and macOS. On both of them, I want to install apps, create directories, copy files, create symlinks for configs, and decrypt secrets (ssh).</p>
<h2 id="heading-plan">Plan</h2>
<p>To keep everything organized, I will use roles for different groups of tasks. The only requirement is to have Ansible and <code>yay</code> or <code>brew</code> installed, depending on the system.</p>
<h2 id="heading-collections">Collections</h2>
<p>Let's start with the required collections. As I am using Arch Linux and yay, I will use <code>kewlfft.aur</code>, and for macOS and brew, <code>community.general</code>. So, <code>requirements.yml</code> will have this:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">collections:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">community.general</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">kewlfft.aur</span>
</code></pre>
<h2 id="heading-inventory-2">Inventory</h2>
<p>For the localhost machine, the <code>inventory.yml</code> file will be simple. Remember to use the correct <code>user</code> and include <code>ansible_connection: local</code>, so Ansible will not use SSH.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">home:</span>
  <span class="hljs-attr">hosts:</span>
    <span class="hljs-attr">pc:</span>
      <span class="hljs-attr">ansible_host:</span> <span class="hljs-string">localhost</span>
      <span class="hljs-attr">ansible_user:</span> <span class="hljs-string">isur</span>
      <span class="hljs-attr">ansible_connection:</span> <span class="hljs-string">local</span>
</code></pre>
<h2 id="heading-playbook-1">Playbook</h2>
<p>The playbook will just load roles. For better organization, I am splitting all tasks into a few roles:</p>
<ul>
<li><p>general - some general stuff that I will always install</p>
</li>
<li><p>tiling - setting up tiling managers and the system for that usage</p>
</li>
<li><p>dev - everything I need for software development work</p>
</li>
<li><p>gaming - some gaming stuff</p>
</li>
</ul>
<p>So the <code>playbook.yml</code> file will look like this:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">System</span> <span class="hljs-string">Setup</span>
  <span class="hljs-attr">hosts:</span> <span class="hljs-string">home</span>
  <span class="hljs-attr">roles:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">general</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">tiling</span> 
    <span class="hljs-bullet">-</span> <span class="hljs-string">dev</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">gaming</span>
</code></pre>
<h2 id="heading-roles">Roles</h2>
<p>Each role will be prepared for both systems, Arch Linux and macOS. The Ansible fact <code>ansible_os_family</code> for Arch is <code>Archlinux</code> and for macOS is <code>Darwin</code>. With <code>when</code> and this fact, we can detect which task to run on which system.</p>
<p>I will not cover my entire setup here, but I will give some examples of how to do some tasks.</p>
<h3 id="heading-installation-role">Installation role</h3>
<p>Let's install <code>discord</code> as an example. First, we need to create a <code>discord</code> role in the <code>roles</code> directory and a <code>main.yml</code> task.</p>
<p><code>roles/discord/tasks/main.yml</code></p>
<p>And in this task, we need to remember both systems.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Darwin</span> <span class="hljs-string">|</span> <span class="hljs-string">Install</span> <span class="hljs-string">Discord</span> <span class="hljs-string">from</span> <span class="hljs-string">brew</span>
  <span class="hljs-attr">community.general.homebrew_cask:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">discord</span>
    <span class="hljs-attr">state:</span> <span class="hljs-string">present</span>
    <span class="hljs-attr">accept_external_apps:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">when:</span> <span class="hljs-string">ansible_os_family</span> <span class="hljs-string">==</span> <span class="hljs-string">'Darwin'</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Arch</span> <span class="hljs-string">|</span> <span class="hljs-string">Install</span> <span class="hljs-string">Discord</span> <span class="hljs-string">from</span> <span class="hljs-string">aur</span>
  <span class="hljs-attr">kewlfft.aur.aur:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">discord</span>
    <span class="hljs-attr">use:</span> <span class="hljs-string">yay</span>
    <span class="hljs-attr">state:</span> <span class="hljs-string">present</span>
  <span class="hljs-attr">when:</span> <span class="hljs-string">ansible_os_family</span> <span class="hljs-string">==</span> <span class="hljs-string">'Archlinux'</span>
</code></pre>
<p>And that's it. For Arch, it will install <code>discord</code> using <code>yay</code>, and for Mac, it will use <code>brew</code>. If the app is already installed on Mac in a different way, <code>accept_external_apps</code> will not raise an error in that case.</p>
<p>If tasks are more complex, we can split up the file into different files per system.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Darwin</span> <span class="hljs-string">|</span> <span class="hljs-string">Install</span> <span class="hljs-string">Discord</span>
  <span class="hljs-attr">ansible.builtin.include_tasks:</span> <span class="hljs-string">"./darwin.yml"</span>
  <span class="hljs-attr">when:</span> <span class="hljs-string">ansible_os_family</span> <span class="hljs-string">==</span> <span class="hljs-string">'Darwin'</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Arch</span> <span class="hljs-string">|</span> <span class="hljs-string">Include</span> <span class="hljs-string">Linux</span> <span class="hljs-string">specific</span> <span class="hljs-string">tasks</span>
  <span class="hljs-attr">ansible.builtin.include_tasks:</span> <span class="hljs-string">"./arch.yml"</span>
  <span class="hljs-attr">when:</span> <span class="hljs-string">ansible_os_family</span> <span class="hljs-string">==</span> <span class="hljs-string">'Archlinux'</span>
</code></pre>
<p>And put the tasks defined earlier into <code>arch.yml</code> and <code>darwin.yml</code>.</p>
<p>This will load all tasks from the file specified in <code>ansible.builtin.include_tasks</code>.</p>
<p>The file tree right now would look like this:</p>
<pre><code class="lang-bash">.
├── collections.yml
├── inventory.yml
├── play.yml
└── roles
    └── discord
        └── tasks
            ├── arch.yml
            ├── darwin.yml
            └── main.yml
</code></pre>
<p>This is a simple installation without any additional steps. It might be a little too much work to create a <code>role</code> for each app if we just want to install a few things without anything extra.</p>
<p>We can do this in one role, even in one task, using loops. For example, let's create a role <code>communication</code> which will install apps for communication: <code>discord</code>, <code>slack</code>, and <code>thunderbird</code>.</p>
<p>The system selection task will look exactly the same as above.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Arch</span> <span class="hljs-string">|</span> <span class="hljs-string">Install</span> <span class="hljs-string">communication</span> <span class="hljs-string">apps</span>
  <span class="hljs-attr">kewlfft.aur.aur:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ app }}</span>"</span>
    <span class="hljs-attr">use:</span> <span class="hljs-string">yay</span>
    <span class="hljs-attr">state:</span> <span class="hljs-string">latest</span>
  <span class="hljs-attr">loop:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">discord</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">slack-desktop</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">thunderbird</span>
  <span class="hljs-attr">loop_control:</span>
    <span class="hljs-attr">loop_var:</span> <span class="hljs-string">app</span>
</code></pre>
<p>Now <code>name</code> instead of the name of the app is the item from the loop. We can define how the variable is named in <code>loop_control</code> and <code>loop_var</code>. Without defining the variable name, it will be <code>item</code>. This loop will run for each item specified under <code>loop</code>.</p>
<p>We can also move those items into variables.</p>
<p>To do this, we need to create a directory <code>vars</code> in the role and <code>main.yml</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">arch_apps:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">discord</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">slack-desktop</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">thunderbird</span>

<span class="hljs-attr">darwin_apps:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">discord</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">slack</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">thunderbird</span>
</code></pre>
<p>Now instead of passing a list of items, we pass a variable:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Arch</span> <span class="hljs-string">|</span> <span class="hljs-string">Install</span> <span class="hljs-string">communication</span> <span class="hljs-string">apps</span>
  <span class="hljs-attr">kewlfft.aur.aur:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ app }}</span>"</span>
    <span class="hljs-attr">use:</span> <span class="hljs-string">yay</span>
    <span class="hljs-attr">state:</span> <span class="hljs-string">latest</span>
  <span class="hljs-attr">loop:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ arch_apps }}</span>"</span>
  <span class="hljs-attr">loop_control:</span>
    <span class="hljs-attr">loop_var:</span> <span class="hljs-string">app</span>
</code></pre>
<p>This way, instead of creating multiple roles for a simple installation process, we can just do it like this.</p>
<pre><code class="lang-bash">.
├── collections.yml
├── inventory.yml
├── play.yml
└── roles
    ├── communication
    │   ├── tasks
    │   │   ├── arch.yml
    │   │   ├── darwin.yml
    │   │   └── main.yml
    │   └── vars
    │       └── main.yml
    └── discord
        └── tasks
            ├── arch.yml
            ├── darwin.yml
            └── main.yml
</code></pre>
<p>There are different ways to organize this stuff; everything is up to you.</p>
<h3 id="heading-symlinkcopy-files">Symlink/copy files</h3>
<p>What if I want to configure my apps and I have some <code>dotfiles</code>?</p>
<p>Let's configure the tiling manager. I am using <code>i3</code> on Arch and <code>aerospace</code> on MacOS. We can create different roles for them, or just a role <code>tiling</code> and store everything there.</p>
<p>Inside the role directory, we will now also need a <code>files</code> directory where we store our config files.</p>
<p>Select the system as before and split arch and darwin into <code>arch.yml</code> and <code>darwin.yml</code>.</p>
<p>The file tree will now look like:</p>
<pre><code class="lang-bash">.
├── collections.yml
├── inventory.yml
├── play.yml
└── roles
    ├── communication
    │   ├── tasks
    │   │   ├── arch.yml
    │   │   └── main.yml
    │   └── vars
    │       └── main.yml
    ├── discord
    │   └── tasks
    │       ├── arch.yml
    │       ├── darwin.yml
    │       └── main.yml
    └── tiling
        ├── files
        │   ├── aerospace.toml
        │   └── i3
        │       └── config.conf
        └── tasks
            ├── arch.yml
            ├── darwin.yml
            └── main.yml
</code></pre>
<p>Tasks for installing tiling stuff look exactly like before with the communication role. But this time, we need some config files:</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Arch</span> <span class="hljs-string">|</span> <span class="hljs-string">Install</span> <span class="hljs-string">tiling</span>
  <span class="hljs-attr">kewlfft.aur.aur:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ item }}</span>"</span>
    <span class="hljs-attr">use:</span> <span class="hljs-string">yay</span>
    <span class="hljs-attr">state:</span> <span class="hljs-string">present</span>
  <span class="hljs-attr">loop:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">i3-wm</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">rofi</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">feh</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Arch</span> <span class="hljs-string">|</span> <span class="hljs-string">Config</span> <span class="hljs-string">tiling</span>
  <span class="hljs-attr">ansible.builtin.file:</span>
    <span class="hljs-attr">src:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ role_path }}</span>/files/<span class="hljs-template-variable">{{ item.src }}</span>"</span>
    <span class="hljs-attr">dest:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ item.dest }}</span>"</span>
    <span class="hljs-attr">state:</span> <span class="hljs-string">link</span>
  <span class="hljs-attr">loop:</span>
    <span class="hljs-bullet">-</span> { <span class="hljs-attr">src:</span> <span class="hljs-string">"i3"</span>, <span class="hljs-attr">dest:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ ansible_env.HOME }}</span>/.config/i3"</span> }
    <span class="hljs-bullet">-</span> { <span class="hljs-attr">src:</span> <span class="hljs-string">"rofi"</span>, <span class="hljs-attr">dest:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ ansible_env.HOME }}</span>/.config/rofi"</span> }
</code></pre>
<p>Using the <code>ansible.builtin.file</code> module, we can define where files should be. <code>src</code> points to the source for the file. The <code>role_path</code> variable points to the role path. <code>dest</code> is the destination, and <code>state: link</code> makes it a symbolic link.</p>
<p>This way, both <code>~/.config/i3</code> and <code>~/.config/rofi</code> are symbolic links to the config in our Ansible files.</p>
<p>If we want to copy files instead of making symbolic links, we can use the <code>ansible.builtin.copy</code> module with <code>src</code> and <code>dest</code>. We can also define permissions by using <code>mode</code> with permission numbers.</p>
<h3 id="heading-decrypt-files">Decrypt files</h3>
<p>What if I tell you that we can store our secrets in a public repository?</p>
<p>For this example, I will set up SSH keys.</p>
<p>Let's create a role <code>ssh</code> and put our SSH key in the <code>files</code> directory.</p>
<p>The role directory will look like this:</p>
<pre><code class="lang-bash">    ├── ssh
    │   ├── files
    │   │   ├── id_rsa
    │   │   └── id_rsa.pub
    │   └── tasks
    │       └── main.yml
</code></pre>
<p>First, we need to encrypt the files. Go to the directory with the files and:</p>
<pre><code class="lang-bash">ansible-vault encrypt id_rsa id_rsa.pub
</code></pre>
<p>Set the password and confirm. Done, now the files are encrypted. Or, if you save your password to a file (e.g., in <code>~/.vault_pass</code>):</p>
<pre><code class="lang-bash">ansible-vault encrypt id_rsa id_rsa.pub --vault-password-file=<span class="hljs-variable">$HOME</span>/.vault_pass
</code></pre>
<p>THIS PASSWORD IS SUPER SECRET, NEVER PUT IT INTO THE REPOSITORY.</p>
<p><code>encrypt</code> will encrypt the file. You can also use <code>edit</code>, <code>view</code>, and <code>decrypt</code> commands.</p>
<p>Now the files are safely encrypted. We can use them in our task to copy to the correct place in our system.</p>
<p>Let's go to <code>roles/ssh/tasks/main.yml</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Ensure</span> <span class="hljs-string">that</span> <span class="hljs-string">ssh</span> <span class="hljs-string">directory</span> <span class="hljs-string">exists</span>
  <span class="hljs-attr">ansible.builtin.file:</span>
    <span class="hljs-attr">path:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ ansible_env.HOME }}</span>/.ssh"</span>
    <span class="hljs-attr">state:</span> <span class="hljs-string">directory</span>
    <span class="hljs-attr">mode:</span> <span class="hljs-string">'700'</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Find</span> <span class="hljs-string">SSH</span> <span class="hljs-string">keys</span> <span class="hljs-string">and</span> <span class="hljs-string">config</span> <span class="hljs-string">files</span>
  <span class="hljs-attr">ansible.builtin.find:</span>
    <span class="hljs-attr">paths:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ role_path }}</span>/files"</span>
    <span class="hljs-attr">file_type:</span> <span class="hljs-string">file</span>
  <span class="hljs-attr">register:</span> <span class="hljs-string">found_files</span>

<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">SSH</span> <span class="hljs-string">Config</span>
  <span class="hljs-attr">ansible.builtin.copy:</span>
    <span class="hljs-attr">src:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ item.path }}</span>"</span>
    <span class="hljs-attr">dest:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ ansible_env.HOME }}</span>/.ssh/<span class="hljs-template-variable">{{ item.path | basename }}</span>"</span>
    <span class="hljs-attr">decrypt:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">mode:</span> <span class="hljs-string">'600'</span>
  <span class="hljs-attr">loop:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ found_files.files }}</span>"</span>
</code></pre>
<p>This will make sure that the <code>~/.ssh</code> directory exists. Find all the files we want to copy and copy them to the correct place. What is important here:</p>
<ul>
<li><p><code>mode</code> needs correct permissions so our key will work correctly;</p>
</li>
<li><p><code>decrypt</code> - will copy decrypted files;</p>
</li>
</ul>
<p>But how do we pass the password to the task?</p>
<p>It can be done with a password file.</p>
<h2 id="heading-run">Run</h2>
<p>If we have everything ready we can run this playbook.</p>
<p>First, install all required collections:</p>
<pre><code class="lang-bash">ansible-galaxy install -r requirements.yml
</code></pre>
<p>Now, to run the playbook:</p>
<pre><code class="lang-bash">ansible-playbook playbook.yml -i inventory.yml --vault-password-file=<span class="hljs-variable">$HOME</span>/.vault_pass
</code></pre>
<p>So now we have everything we wanted: installing apps, copying and symlinking files, creating directories, and decrypting secrets. Everything is set up in a way that we can use on multiple systems. If we use symlinks for configs and have some common configs between all systems, updating them in the repository will help us keep them on all machines.</p>
<p>If you would like to see my full setup: <a target="_blank" href="https://github.com/Isur/dotfiles">https://github.com/Isur/dotfiles</a></p>
]]></content:encoded></item><item><title><![CDATA[Simplify Version Management: Switching from NVM and Pyenv to Proto]]></title><description><![CDATA[Context
When you are working on just one project, you probably don't need to worry about installing different versions of languages and tools on your machine. But if you are working on multiple projects, some of them might require different versions,...]]></description><link>https://blog.isur.dev/simplify-version-management-switching-from-nvm-and-pyenv-to-proto</link><guid isPermaLink="true">https://blog.isur.dev/simplify-version-management-switching-from-nvm-and-pyenv-to-proto</guid><category><![CDATA[Node.js]]></category><category><![CDATA[Python]]></category><category><![CDATA[nvm]]></category><category><![CDATA[pyenv]]></category><category><![CDATA[tools]]></category><category><![CDATA[Developer]]></category><dc:creator><![CDATA[Artur Bednarczyk]]></dc:creator><pubDate>Sun, 19 May 2024 13:06:32 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-context">Context</h1>
<p>When you are working on just one project, you probably don't need to worry about installing different versions of languages and tools on your machine. But if you are working on multiple projects, some of them might require different versions, especially for languages that frequently update.</p>
<p>I mostly work with <code>node.js</code> and <code>python</code>. Some projects I handle require different versions of these technologies. This includes creating new projects and maintaining older ones. Switching between projects also means switching between different versions of the technologies.</p>
<p>With <code>node.js</code>, I have always used <code>nvm</code> to change the version of <code>node</code> when necessary. For <code>python</code>, I found that <code>pyenv</code> was helpful. However, it was sometimes problematic to install different tools and versions, and to remember to switch between them.</p>
<p>Automating this process is possible but requires a few extra steps. With <code>nvm</code>, we can create a script that runs with the <code>cd</code> command to check for an <code>.nvmrc</code> file or use a shell plugin. With the correct setup, <code>pyenv</code> will automatically choose the right <code>python</code> version.</p>
<p>So, we can easily manage those versions, right? Right. For each language, there is a tool to manage multiple versions installed on the system. We can find the right tool, learn how to use it, and apply it. But can we simplify and standardize this solution across different languages?</p>
<h1 id="heading-proto">Proto</h1>
<p>Lately, I discovered a tool called <code>proto</code> - "proto is a pluggable version manager, a unified toolchain." This is quote from the tool documentation that can be found here:</p>
<p><a target="_blank" href="https://moonrepo.dev/proto">https://moonrepo.dev/proto</a></p>
<p>Basically, it is a tool to manage versions for different programming languages and other tools. It supports multiple languages, detects versions based on context, and verifies checksums to ensure the source is trusted. It is also cross-platform and allows for custom tooling through plugins.</p>
<p>Since it supports <code>node.js</code>, <code>npm</code>, <code>pnpm</code>, <code>yarn</code>, and <code>python</code>(this one is still as "experimental"), I decided to give it a try.</p>
<h2 id="heading-installation">Installation</h2>
<p>Before installation, there are a few requirements. You need to have <code>git</code>, <code>unzip</code>, <code>gzip</code>, and <code>xz</code> installed. These tools are used to fetch versions and work with archives. You can easily install them using system package managers like <code>brew</code> or <code>apt</code>.</p>
<p>To install <code>proto</code>, the authors provide a script:</p>
<pre><code class="lang-bash">curl -fsSL https://moonrepo.dev/install/proto.sh | bash
</code></pre>
<p>It will check if the requirements are met and then install <code>proto</code>.</p>
<p>That's it, now <code>proto</code> is installed. It will also automatically add the necessary paths to your shell profile, like this:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> PROTO_HOME=<span class="hljs-string">"<span class="hljs-variable">$HOME</span>/.proto"</span>
<span class="hljs-built_in">export</span> PATH=<span class="hljs-string">"<span class="hljs-variable">$PROTO_HOME</span>/shims:<span class="hljs-variable">$PROTO_HOME</span>/bin:<span class="hljs-variable">$PATH</span>"</span>
</code></pre>
<p>Since everything will be stored in <code>~/.proto/</code>, it will be easy to uninstall by deleting that directory and removing those exports from your profile.</p>
<h2 id="heading-version-detection">Version detection</h2>
<p>Now, when we go to our project, <code>proto</code> needs to detect which versions of tools we are using. It can find this information from the tool's ecosystem files, like <code>.nvmrc</code> or <code>package.json</code>. Alternatively, it can be specified in a <code>.prototools</code> file. The file structure is simple; it just contains <code>tool = version</code>.</p>
<p>An example <code>.prototools</code> file might look like this:</p>
<pre><code class="lang-bash">node = <span class="hljs-string">"22.1.0"</span>
pnpm = <span class="hljs-string">"9.1.1"</span>
python = <span class="hljs-string">"3.8"</span>
</code></pre>
<p>There are also other ways to specify versions.</p>
<p>Command line:</p>
<pre><code class="lang-bash">proto run node 22.1.0
</code></pre>
<p>Environment variable:</p>
<pre><code class="lang-bash">PROTO_NODE_VERSION=22.1.0 proto run node
</code></pre>
<p>Or global versions that are stored at <code>~/.proto/.prototools</code>.</p>
<p>If the version cannot be detected or found, it will show an error.</p>
<h2 id="heading-version-pin">Version pin</h2>
<p>The <code>.prototools</code> file can be modified manually, but you can also use the <code>proto pin</code> command to save versions to the file. For example:</p>
<pre><code class="lang-bash">proto pin node 19
</code></pre>
<p>Will save:</p>
<pre><code class="lang-bash">node = <span class="hljs-string">"~19"</span>
</code></pre>
<p>Using flag <code>--global</code> will <code>pin</code> the version globally in <code>~/.proto/.prototools</code>.</p>
<h2 id="heading-auto-install-and-settings">Auto Install and settings</h2>
<p>By default, it will not automatically install all tools. You can do it manually using the command:</p>
<pre><code class="lang-bash">proto use
</code></pre>
<p>This will download and install all the tools and plugins specified in the <code>.prototools</code> file.</p>
<p>However, you can enable automatic installation by changing the settings in <code>.prototools</code>.</p>
<pre><code class="lang-bash">[settings]
auto-install = <span class="hljs-literal">true</span>
telemetry = <span class="hljs-literal">false</span>
</code></pre>
<p>Or with the environment variable <code>PROTO_AUTO_INSTALL</code>.</p>
<p>This way, if a required tool is specified but not available on the system, it will be installed automatically.</p>
<p>There are more settings available, such as <code>detect-strategy</code>, which lets you choose how to detect versions, and <code>telemetry</code>, which collects anonymous usage data (the default is true, but you can turn it off by setting it to false). Other settings are listed in the <a target="_blank" href="https://moonrepo.dev/docs/proto/config#available-settings">documentation</a>.</p>
<h2 id="heading-installing-tools-manually">Installing tools manually</h2>
<p>To manually install, we can use the <code>proto install</code> command with the tool and version as arguments:</p>
<pre><code class="lang-bash">proto install python 3.11.9
</code></pre>
<p>But what versions are available to install? To check that, use the command <code>proto list-remote</code> with the tool as an argument:</p>
<pre><code class="lang-bash">proto list-remote python
</code></pre>
<p>This will list all available versions to install. To list all installed versions, use a similar command <code>proto list</code> with the tool as an argument.</p>
<p>This is problematic for <code>python</code>, which is supported (experimental). <code>proto</code> installs only pre-built versions and <code>python</code> doesn't support all of them. Building from source will be supported in the future, as stated on the plugin's <code>GitHub</code> page.</p>
<p>In the <code>python</code> plugin repository on GitHub, I found a list of available versions:</p>
<p><a target="_blank" href="https://raw.githubusercontent.com/moonrepo/python-plugin/master/releases.json">https://raw.githubusercontent.com/moonrepo/python-plugin/master/releases.json</a></p>
<p>To make it easier to use, if you have installed <code>jq</code>, you can use it:</p>
<pre><code class="lang-bash">curl https://raw.githubusercontent.com/moonrepo/python-plugin/master/releases.json | jq <span class="hljs-string">'keys'</span>
</code></pre>
<p>This command will list all the versions that can be downloaded.</p>
<h2 id="heading-plugin-installation-with-gojq-example">Plugin installation with gojq example</h2>
<p>If you don't have <code>jq</code> installed, you can use <code>.proto</code> to install <code>gojq</code> (a <code>go</code> implementation of <code>jq</code>).</p>
<pre><code class="lang-bash">proto plugin add gojq <span class="hljs-string">"source:https://raw.githubusercontent.com/stk0vrfl0w/proto-toml-plugins/main/plugins/gojq.toml"</span>
--global
</code></pre>
<p>This will add the globally available plugin for <code>gojq</code>, and now you can install it:</p>
<pre><code class="lang-bash">proto install gojq --pin --global
</code></pre>
<p>This will install <code>gojq</code> globally and pin its version in <code>~/.proto/.prototools</code>.</p>
<p>Now, you can use the previous command to list all <code>python</code> versions that can be installed with <code>proto</code> using <code>gojq</code> instead of <code>jq</code>.</p>
<pre><code class="lang-bash">curl https://raw.githubusercontent.com/moonrepo/python-plugin/master/releases.json | gojq <span class="hljs-string">'keys'</span>
</code></pre>
<h2 id="heading-supported-tools">Supported tools</h2>
<p>Besides <code>node.js</code>, <code>npm</code>, <code>pnpm</code>, <code>yarn</code>, and experimental <code>python</code>, there are more supported tools. Built-in support is also available for <code>bun</code>, <code>deno</code>, <code>go</code>, and <code>rust</code>. Additionally, many more tools are available as third-party plugins (like the example of <code>gojq</code> above). You can find a complete list in the documentation here:</p>
<p><a target="_blank" href="https://moonrepo.dev/docs/proto/tools">https://moonrepo.dev/docs/proto/tools</a></p>
<p>So, if I want to try another language like <code>go</code> or <code>rust</code>, I will use <code>proto</code> to install it.</p>
<h1 id="heading-summary">Summary</h1>
<blockquote>
<p>Managing multiple versions of programming languages and tools can be challenging, especially when working on various projects that require different versions. While tools like nvm for Node.js and pyenv for Python help, they can be cumbersome to manage. Proto offers a unified solution for version management across multiple languages and tools, including Node.js, npm, pnpm, yarn, and experimental support for Python. It automates version detection and installation, supports custom tooling through plugins, and simplifies the process with a single configuration file. This guide covers the installation, configuration, and usage of Proto to streamline your development workflow.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Validate your translations and other files with file-json-validator]]></title><description><![CDATA[TL: DR
npm package file-json-validator to validate file structure and json keys:

https://www.npmjs.com/package/file-json-validator

https://github.com/Isur/file-json-validator


Example use case - check if your translations have the same files/keys ...]]></description><link>https://blog.isur.dev/validate-your-translations-and-other-files-with-file-json-validator</link><guid isPermaLink="true">https://blog.isur.dev/validate-your-translations-and-other-files-with-file-json-validator</guid><category><![CDATA[i18n]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[CI/CD]]></category><category><![CDATA[npm]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Artur Bednarczyk]]></dc:creator><pubDate>Sun, 21 Apr 2024 08:00:17 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-tl-dr">TL: DR</h1>
<p>npm package <code>file-json-validator</code> to validate file structure and <code>json</code> keys:</p>
<ul>
<li><p><a target="_blank" href="https://www.npmjs.com/package/file-json-validator">https://www.npmjs.com/package/file-json-validator</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/Isur/file-json-validator">https://github.com/Isur/file-json-validator</a></p>
</li>
</ul>
<p>Example use case - check if your translations have the same files/keys for each language.</p>
<h1 id="heading-context">Context</h1>
<p>A web application that you and your team are creating is growing and you are adding new languages. Some tools and packages will help you manage that in your app. Often you will have some <code>json</code> files that will contain all your translations.</p>
<p>It might be something like this:</p>
<pre><code class="lang-bash">./public
└── locales
    ├── en
    │   ├── buttons.json
    │   └── common.json
    ├── ger
    │   ├── buttons.json
    │   └── common.json
    └── pl
        └── common.json
</code></pre>
<p>Sometimes it might be a little problematic.</p>
<h1 id="heading-problem">Problem</h1>
<p>As you can see in the example file structure above, one <code>json</code> file is missing. In this simple example, this is not an issue; it's easy to find and easy to fix. However, consider a larger codebase with more files, or larger files containing numerous keys, possibly even nested keys. It's easy to forget to add a key to one of the <code>json</code> files or make a typo. If you don't check all translations, you might not notice that something is missing - your texts could be missing or falling back to another language.</p>
<p>This is something that happened in some of the projects I was involved in. We had to look through files to find what is wrong. So I thought about a solution that will help us.</p>
<h1 id="heading-solution">Solution</h1>
<p>How can you avoid issues with missing keys and files, especially when dealing with a lot of translations? This is where I came up with an idea that something like a linter would be a good addition to the CI. When someone modifies translations and pushes the code to our repository with GitHub actions enabled, it will automatically verify everything. To achieve this, I developed a CLI tool to handle the task.</p>
<p>The package and its source code can be found here:</p>
<ul>
<li><p>https://www.npmjs.com/package/file-json-validator</p>
</li>
<li><p>https://github.com/Isur/file-json-validator</p>
</li>
</ul>
<h1 id="heading-usage">Usage</h1>
<h2 id="heading-installation">Installation</h2>
<p>To use it in the project install it as (dev) dependency and use <code>fjv</code> as a CLI tool.</p>
<pre><code class="lang-bash">pnpm add file-json-validator
</code></pre>
<h2 id="heading-cli-commands">CLI commands</h2>
<p>There are a few commands that you can use depending on what you want to check:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Command</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td><code>fjv compare</code></td><td>Compare the content of directories inside the selected directory.</td></tr>
<tr>
<td><code>fjv dir</code></td><td>Compare selected directories.</td></tr>
<tr>
<td><code>fjv json</code></td><td>Compare selected <code>json</code> files.</td></tr>
</tbody>
</table>
</div><h2 id="heading-examples">Examples</h2>
<p>If your file structure is like this:</p>
<pre><code class="lang-bash">./public
└── locales
    ├── en
    │   ├── buttons.json
    │   └── common.json
    ├── ger
    │   ├── buttons.json
    │   └── common.json
    └── pl
        └── common.json
</code></pre>
<p>Then you can use CLI:</p>
<pre><code class="lang-bash">fjv compare ./public/locales en
</code></pre>
<p>The first argument is the command <code>compare</code>, second is pointing at the directory containing all other language directories and the last argument is the default language that others will be compared with.</p>
<p>Example output for this command will be like:</p>
<pre><code class="lang-bash">Directory structure: (1 errors)
./public/locales/pl
         -buttons.json
./public/locales/ger  ==&gt; OK
Json content: (3 errors)
./public/locales/ger/buttons.json  ==&gt; OK
./public/locales/pl/common.json  ==&gt; OK
./public/locales/ger/common.json
         -calc.minus
         -calc.equal
         +no
</code></pre>
<p>It shows files that are correct and highlights any differences that shouldn't exist. If there is a <code>-</code>, it means something is missing. If there is a <code>+</code>, it means there is something extra that shouldn't be there.</p>
<p>If there are any differences, it will exit with an error - which is useful in GitHub actions.</p>
<p>Similar behavior with the second command: <code>dir</code>.</p>
<pre><code class="lang-bash">fjv dir ./public/locales/en ./public/locales/pl ./public/locales/ger
</code></pre>
<p>The first argument is the main language and the rest are other selected languages. The result is in the same format as for <code>compare</code> command.</p>
<p>The last one is <code>json</code> where the arguments are just <code>json</code> files.</p>
<pre><code class="lang-bash">fjv json ./public/locales/en/common.json ./public/locales/pl/common.json ./public/locales/ger/common.json
</code></pre>
<p>Where again, the first one is the main, the rest are compared to it.</p>
<h2 id="heading-flags">Flags</h2>
<p>For all commands, there are available some flags to add:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Flag</td><td>Description</td><td>Command</td></tr>
</thead>
<tbody>
<tr>
<td><code>--only-warn</code></td><td>Do not exit with an error if there are any diffs.</td><td><code>compare</code>, <code>dir</code>, <code>json</code></td></tr>
<tr>
<td><code>--show-only-errors</code></td><td>Show only errors.</td><td><code>compare</code>, <code>dir</code>, <code>json</code></td></tr>
<tr>
<td><code>--only-structure</code></td><td>Check only file structure.</td><td><code>compare</code>, <code>dir</code></td></tr>
<tr>
<td><code>--only-json</code></td><td>Check only <code>json</code> structure.</td><td><code>compare</code>, <code>dir</code></td></tr>
</tbody>
</table>
</div><h3 id="heading-example-with-flags">Example with flags:</h3>
<pre><code class="lang-bash">fjv compare ./public/locales en --only-warn --only-show-errors
</code></pre>
<p>Will not exit with an error if there are any differences in files, and will display only the differences.</p>
<pre><code class="lang-bash">Directory structure: (1 errors)
./public/locales/pl
         -buttons.json
Json content: (3 errors)
./public/locales/ger/common.json
         -calc.minus
         -calc.equal
         +no
</code></pre>
<h2 id="heading-ci">CI</h2>
<p>This may be used in CI, for example, what I do is add to the <code>package.json</code> script:</p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
    <span class="hljs-attr">"translation-check"</span>: <span class="hljs-string">"fjv compare ./public/locales en"</span>
}
</code></pre>
<p>I run it in CI just like tests or a linter. If something is wrong, it will break your workflow. Developers can also use the script to verify if everything is correct when they make changes to translations.</p>
<h1 id="heading-api">API</h1>
<p>If you prefer to use it differently, you can also use it in your node project. There are a few functions (with types) that are exported.</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> {
  compareJsonsInDirs,
  compareDirectoriesContent,
  compareJsonsFiles,
  compareJsonObjects,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"file-json-validator"</span>;
</code></pre>
<ul>
<li><p><code>compareJsonsInDirs</code> - compare <code>json</code> files in directories.</p>
</li>
<li><p><code>compareDirectoriesContent</code> - check if directories have the same files.</p>
</li>
<li><p><code>compareJsonsFiles</code> - compare <code>json</code> files.</p>
</li>
<li><p><code>compareJsonObjects</code> - compare <code>json</code> objects.</p>
</li>
</ul>
<h1 id="heading-summary">Summary</h1>
<p>npm package <code>file-json-validator</code> helps validate file structure and <code>json</code> keys - it can be used for translations in applications. It provides CLI commands to compare directories and <code>json</code> files with some flag so you can customize your experience with it. Easy to integrate into CI workflow for automating validation.</p>
]]></content:encoded></item></channel></rss>