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…”
- “Fix null pointer in auth middleware” (applying this will fix…)
- “Add retry logic to the HTTP client” (applying this will add…)
- “Remove deprecated config option” (applying this will remove…)
Not:
- “Fixed null pointer…” (past tense; describes what you did, not what the commit does)
- “Fixing null pointer…” (present continuous; sounds like a work-in-progress)
- “Null pointer fix” (noun phrase; vague, no action)
Properties of a good continuous delivery (CD) pipeline
- Fast: Keep feedback loops small to avoid context switching. We should also fail fast (run risky and/or fast tests early).
- Reliable and deterministic: it should work every time, without flakiness. Flaky tests are often worse than no test as it means you cannot rely on your test results.
- Automated and comprehensive: no manual steps between commit and production. No additional steps needed to get to production.
- The one and only path to production: nothing is deloyed except through the pipeline.
- Green: if the pipeline is red, getting it back to green is the highest priority. Usually whoever breaks it fixes it. Automated rollbacks are the alternative.
- Based on artifacts, not environments: the same artifact should be built, tested, pushed through environments, and deployed to production.
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:
SIGUSR1: toggle / show / hide stateSIGUSR2: reload config
Window manager vs. session manager vs. display server vs. display manager
I find these fairly confusing, so just to summarize the layers:
Display server: the process that talks to the GPU and input devices and owns the screen. It’s the protocol everything else draws through. I use Wayland, which is the newer alternative to X11. I’ve not yet really understood the advantages though.
Compositor: combines the individual window buffers into the final image (transparency, shadows, vsync).
Window manager (WM): decides where windows go and how to manipulate them, such as tiling, floating, focus, borders, etc.I use Hyprland which is both the compositor and window manager.
Display manager (DM): Despite the name, it’s about login and not drawing windows (very confusing). It basically handles the authentication and then launches a session. I use SDDM altough I don’t really know what the differences are to alternatives.
Session manager: handles application lifecycles within a session, can save/restore state. I use UWSM although again I’m not really sure I could tell the difference to any different session manager. Maybe that matters if you actually use multiple graphical sessions?
Desktop environment (DE): usually people don’t choose individual components of the above (I guess they don’t like spending countless hours on their setup?) but just choose a desktop environment that combined all of them such as GNOME or KDE Plasma.
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.)
- Software architecture: the internal structure of a single application such as modules, layers, dependencies, patterns (hexagonal, event-driven).
- System architecture: how multiple components fit together in a working system (services, databases, queues, networks, …) and how they communicate.
- Solution architecture: how to use technology (or maybe not use it) to solve a business problem.
- Enterprise architecture: the organization-wide standards, governance and strategy across systems.
- Data architecture: how data is stored, moved, and governed. Topics include ETL/ELT, batch/streaming, CDC, lineage, pipelines, masking, access controls, …
- Data modelling: how we structure the data itself. Topics include entities, relationships, keys, normal forms, virtualization, shapes (transactional, dimensional, star schema, data vault, …). We often distinguish between conceptual, logical, and physical data models.
- Cloud / infrastructure architecture: the compute, network, and platform everything runs on; physical machines, virtual machines, containers, serverless; think VPCs, load balancers, resilience through multiple regions, IAM, …
- Security architecture: addresses threat modeling, trust boundaries, security controls, …
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:
- Design-time modularity is about the code architecture and design. Are the boundaries and interfaces clear? What about the separation of concerns? Can we reason about each of the parts without having to understand all the other parts?
- Deploy/run-time modularity: is about the independent deployability mentioned above.
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.