Nuggets & TIL

Small pieces of config, conventions, and TILs (today-I-learned moments) I keep coming back to. Easy to forget, worth writing down.

Commit messages should complete a sentence

A good commit message completes the sentence: “Applying this change will…”

Not:

Properties of a good continuous delivery (CD) pipeline

Unlock account after too many failed password attempts (Arch Linux)

Arch Linux uses pam_faillock to temporarily lock an account after repeated failed logins. The symptom: the correct password still gets rejected.

Reset the counter with:

faillock --user <username> --reset

If you’re locked out of your own session entirely, switch to another TTY (Ctrl+Alt+F2), log in as root, and run it from there.

To see the current state before resetting:

faillock --user <username>

The thresholds (number of attempts, lockout duration) are configured in /etc/security/faillock.conf. Usually I increase it the first time this happens to me…

SSH agent inside devcontainers

SSH keys should be managed by an agent, not loaded directly. The typical failure mode: keys work on the host, but disappear inside a devcontainer.

The fix is two-part. First, start the agent in your login shell profile (~/.profile or ~/.bash_profile, not ~/.bashrc — that one only runs in interactive non-login shells and won’t be sourced in many devcontainer scenarios):

# ~/.profile
if [ -z "$SSH_AUTH_SOCK" ]; then
    eval "$(ssh-agent -s)"
    ssh-add ~/.ssh/id_ed25519
fi

Second, forward the agent socket into the container. For VS Code devcontainers this is automatic when the Dev Containers extension detects a running agent. If it doesn’t pick it up, make sure SSH_AUTH_SOCK is exported before the container starts. On Linux / WSL you can verify with:

echo $SSH_AUTH_SOCK   # should print a socket path, not be empty
ssh-add -l            # should list your loaded keys

If both look right on the host but the container still can’t see them, check whether the socket path is being mounted. The extension mounts it under $SSH_AUTH_SOCK inside the container by default.

To avoid the agent disappearing in the first place, you can also start it in your profile and save the environment variables to a file. This way, if the agent is already running, you won’t start a new one and lose your loaded keys:

if [ -z "$SSH_AUTH_SOCK" ]; then
    # Check for a currently running instance of the agent
    RUNNING_AGENT="`ps -ax | grep 'ssh-agent -s' | grep -v grep | wc -l | tr -d '[:space:]'`"
    if [ "$RUNNING_AGENT" = "0" ]; then
        # Launch a new instance of the agent
        ssh-agent -s &> $HOME/.ssh/ssh-agent
    fi
    eval `cat $HOME/.ssh/ssh-agent` > /dev/null
    ssh-add 2> /dev/null
fi

Conventional commits in lazygit

Lazygit lets you bind custom commands to keys. A short menu-driven prompt makes it easy to stay consistent with conventional commits without typing the prefix every time.

In ~/.config/lazygit/config.yml:

customCommands:
  - key: "c"
    context: "files"
    description: "Conventional commit"
    loadingText: "Committing..."
    prompts:
      - type: "menu"
        title: "Commit type"
        key: "Type"
        options:
          - name: "feat"
            description: "A new feature"
            value: "feat"
          - name: "fix"
            description: "A bug fix"
            value: "fix"
          - name: "chore"
            description: "Other changes that don't modify src or test files"
            value: "chore"
          - name: "docs"
            description: "Documentation only changes"
            value: "docs"
          - name: "refactor"
            description: "Code change that neither fixes a bug nor adds a feature"
            value: "refactor"
          - name: "style"
            description: "Formatting, whitespace, no code change"
            value: "style"
          - name: "perf"
            description: "Performance improvement"
            value: "perf"
          - name: "test"
            description: "Adding or fixing tests"
            value: "test"
          - name: "build"
            description: "Build system or external dependencies"
            value: "build"
          - name: "ci"
            description: "CI configuration"
            value: "ci"
          - name: "revert"
            description: "Revert a previous commit"
            value: "revert"
      - type: "input"
        title: "Scope (leave empty to skip)"
        key: "Scope"
      - type: "input"
        title: "Commit message"
        key: "Message"
    command: 'git commit -m "{{.Form.Type}}{{ if .Form.Scope }}({{ .Form.Scope }}){{ end }}: {{ .Form.Message }}"'

  - key: "C"
    context: "files"
    description: "Quick commit (plain message)"
    loadingText: "Committing..."
    prompts:
      - type: "input"
        title: "Commit message"
        key: "Message"
    command: 'git commit -m "{{ .Form.Message }}"'

The custom command overrides the built-in c binding (which normally opens the plain commit editor). The original behaviour is preserved on capital C here as a fallback for the cases where the prompt-driven flow gets in the way.

pkill -SIGUSR2 to reload a running program

I often use these commands to reload configs:

pkill -SIGUSR2 waybar
pkill -SIGUSR2 swaync

When pkill -SIGUSR2 waybar instantly reloads waybar’s config without restarting the process, it’s tempting to think SIGUSR2 “means” reload but it is actually just a signal that waybar happens to listen for. Signals are notifications from the kernel to a process and it can decide how to react to them, except for SIGKILL and SIGSTOP which can’t be overridden. SIGUSR1 and SIGUSR2 are deliberately undefined by POSIX but the default action is to terminate the process if it receives them and doesn’t have a handler registered. However, there is a soft convention has emerged across Wayland status bars, notification daemons, and launchers:

Window manager vs. session manager vs. display server vs. display manager

I find these fairly confusing, so just to summarize the layers:

Types of architecture

I usually don’t really care much about titles that people hold, but I deal with a lot of different architects so I made myself this little cheat sheet to try to mentally file what they will be concerned about. (In practice the roles are so varied that I always just ask them anyways what they’re actually responsible for.)

Definition of “legacy code”

Legacy code is “code you’re not comfortable changing” or “valuable code that you’re afraid to change.” (from Nicolas Carlo)

Microservices are about modularity, and modularity has dimensions

Microservices are independently deployable services that work together.

They often get sold as a scalability story, but really they are about modularity: well-defined boundaries with high cohesion inside and loose coupling between.

However, there are also different dimensions of modularity that we can optimize for:

A good microservice architecture should give you both of the above. But you pay for it with a lot of technical and operational overhead. A modular monolith deliberately focuses on the first dimension (have clean modules and boundaries in your system that you could also split out separately) but you can deploy everything together. It is often the better choice for most teams, especially early on, because you get the benefits of clear boundaries without the additional complexity.