Automating and testing dotfiles

Automating and testing dotfiles

August 9, 2020
dotfiles, config, testing, ci

I run multiple Archlinux machines at home and an OSX machine for work, so I need to keep my system configuration in sync. I have a lot of applications tinkered for my workflow — I don’t want to switch computers and have to reconfigure something every time I change in another machine. stow or a bash script could allow us to manage files, but they are limited when a specific non-scripting configuration requires different settings for a specific machine. For example, my Alacritty requires different settings for OSX and Linux.

I solved this issue by keeping multiple branches for multiple machines, but that sucked when I updated a configuration and I needed to sync with my main branch (Linux). Handling merge conflicts or cherry-picking commits was tiring. Eventually, after trying out stow for years and bash scripts, I came up with a neat workflow

Chezmoi to the rescue #

Chezmoi solved the branching issue as it allows me to handle multiple configurations for different machines, by using templates of non-scripting configurations for inserting context before copying the file. This solves the headaches with merge conflicts and having to maintain multiple branches. I now need one branch for keeping my files and let Chezmoi handle the configuration context and changes when required.

Handling installation with Ansible #

Yet, I still have the dependency problem when reinstalling my operating system, I almost always remember the applications I use, but not always all their non-required dependencies, also, as I keep multiple OSes, some program differ. Keeping a bash script in my dotfiles could solve this, but that would require some time to maintain an installation script for each OS.

I choose Ansible to handle my dependency installation. I keep 4 Ansible roles, osx for anything related to osx installation, archlinux for arch related stuff, linux for common Linux commands and common, shared between osx and archlinux (it takes care of cloning external repositories, applying Chezmoi, etc).

This allows me to easily install my configuration in a new machine and update any new dependency I added to my toolkit.

Continuous Integration #

I mentioned before that I am working more from OSX due to my job, so I tend to miss Archlinux package updates/removals, and I always end up having to tinker my Archlinux Ansible roles with the new packages or configuration when they change, again, tiring. I decided to install my dotfiles on a CI, using Github Workflow. My current Github workflow runs 2 jobs, one for Archlinux (running on Ubuntu with Docker) and another with MacOS. Both runs Ansible, installs all dependencies, does some system checks, and finally caches the result.

This allows me to keep up with Archlinux/OSX updates a bit faster and making sure my Ansible is fully functional — if one of the CI fail, something happened with the dependencies or my configuration.

Going down the hole #

With this dotfile structure, I can easily write test scripts for asserting if files were correctly copied, packages installed, etc. For example, I could write a Python script which asserts if files are correctly in place, I could then set this script to run after Ansible did it’s job.

import pathlib
import os
import platform

HOME = pathlib.Path(os.getenv("HOME", ".")).absolute()

def osx_verify_copied_files(home: str = HOME,
                            required=(".zshrc", ".zshenv")):
    for f in required:
        print(f"Checking if {HOME.joinpath(f)} exists")
        assert HOME.joinpath(f).exists() == True, "{f} does not exist"

def osx_verify_hostname(hostname):
    print(f"Verifying if hostname '{hostname}' is set")
    assert platform.node() == hostname, "Hostname does not match"

Checking if /Users/benmezger/.zshrc exists
Checking if /Users/benmezger/.zshenv exists
Verifying if hostname 'benmezger-ckl.local' is set

Emacs org mode #

I live in Emacs, this blog is written in Org mode and Hugo, my snippets are stored in an org file and my code is written in Emacs. To ease my life, I decided to keep a file with general commands I might need when tinkering with my dotfiles. Org mode supports literate programming, so keeping a file allows me to execute the commands in-buffer. I simply C-c C-c in the snippet and let org mode make it happen. This is nice when I am tinkering my dotfiles and I need to apply changes with Chezmoi, for example.

Conclusion #

System configuration is important for a stable workflow, as we don’t want to change much when switching machines, Ansible allows us to keep multiple installations up to date and Chezmoi allows handling these configurations file properly. Keeping your dotfiles in a CI sounds overwhelming, however, it does guarantee your installation scripts are fully functional against multiple OSes and you will know when something bad happened.