Skip to content

Replace Ansible with mise modules - #160

Merged
TechDufus merged 71 commits into
mainfrom
mise-migration
Oct 23, 2026
Merged

TechDufus merged 71 commits into
mainfrom
mise-migration

Conversation

@TechDufus

Copy link
Copy Markdown
Owner

Replaces the Ansible roles/ tree with mise modules (modules/*/mise*.toml) and a slimmed bin/dotfiles that runs mise bootstrap. Supported profiles: omarchy, cachyos, macos.

Opened as a draft for review.

Port every kept role to a mise conf.d module under modules/<name>/,
loaded through ~/.config/mise/conf.d -> ~/.dotfiles/modules and
applied with `mise bootstrap`: [bootstrap.packages], [dotfiles]
links/copies/merges/blocks, [tools], [bootstrap.files], and
bootstrap:<module> tasks. bin/dotfiles is now a small wrapper that
installs git+mise, picks the omarchy/cachyos/macos profile, and
runs `mise bootstrap --yes`.

Removes the Ansible playbook, group_vars, per-role docs, tests that
grep YAML, Ubuntu/Fedora/WSL paths, and the dropped roles. CI
dry-runs both Linux profiles in an Arch container plus macOS, and
runs the module tests.
bootstrap:omarchy caches success via outputs = { auto = true }. Agent and
SSH shells lack HYPRLAND_INSTANCE_SIGNATURE, so a run from there printed
"not in Hyprland" and marked the sources handled without reloading or
validating. Discover the running instance with `hyprctl instances` instead.
herdr/tmux panes that outlive a Hyprland restart keep a dead
HYPRLAND_INSTANCE_SIGNATURE, so `hyprctl reload` failed and stopped the
later bootstrap tasks. Use the variable only when `hyprctl instances`
lists it, else the running instance; name the instance when reload fails.
svelte-language-server needs svelte ^4.2.19, which resolves to 4.2.20.
mise refuses it as a trust downgrade: 4.2.20 was published without
provenance after 5.0.0 had it. Same svelte-admin publisher and an
upstream svelte@4.2.20 tag; 4.2.19 was unattested too. Exclude only
that version so a later unattested release still fails.
mise loads a module folder's mise.toml as project config whenever it runs
there (bootstrap:devtools, or anyone working in the folder), so
node = "lts" overrode Omarchy's global 26.8.2 pin and the task run
installed node@24. Declare node only for CachyOS and macOS, which have
no Omarchy pin.
The old jj role re-rendered user.email from 1Password on every run.
The port only filled an empty value, so a machine that once got the
noreply fallback kept it while git used the real address. Set jj to
the same email as git (1Password, else the last synced value) when it
differs; use noreply only when jj has no email at all.
Deploy modes had been carried over from the Ansible roles as-is. Most
copies dated from each role's first version with no recorded reason,
and a copy silently overwrites live edits on every run.

- Symlink files the app only reads: zsh (per-file links for
  ~/.config/zsh, so deleted sources lose their links), ghostty, kitty,
  lsd, lfk, gh-dash, worktrunk, Taskfile, commit_template, omp
  AGENTS.md, Omarchy keyboard.lua (now in hypr/), and on CachyOS/macOS
  tmux, neofetch, the btop theme, starship, bat, k9s, bash, bordersrc.
- herdr config.toml becomes a merge: its settings screen writes keys
  in place, which every run reverted.
- ~/.npmrc becomes three line edits: the copy deleted npm login
  tokens on every run.
- Copies that remain carry a comment with the reason (app replaces
  the file, rewrites it on exit, or ignores symlinks), and AGENTS.md
  states the rules plus a mise hazard: switching a directory symlink
  to symlink-each with --force-dotfiles writes through the old link
  and replaces repo files with self-referencing links.
bun 1.4.3 made `bun --check` a full type check, which fails in CI where
neither @types/node nor the omp package is installed. On bun 1.4.0 the
step only ever caught parse errors, so keep that with
`bun build --no-bundle`; the behavioural checks below are unchanged.
Centre the logo with a CI status badge, turn the reference-only and
run-twice notes into GitHub callouts, and replace the bootstrap prose
with a Mermaid diagram of what `dotfiles` does.
A failed package install stops the whole mise run before dotfiles,
tools and tasks. Verified in a clean Arch container: with 1Password
in [bootstrap.packages] and unavailable, `mise bootstrap` exited 1 at
the packages step and applied nothing else.

Install the app and CLI from bootstrap:1password instead (pacman when
a repo ships them, as Omarchy does; paru or yay otherwise; the Homebrew
casks on macOS, leaving a hand-installed app alone). Every failure only
warns. Same container, three cases (no repo, no AUR helper, a failing
AUR build): exit 0 with dotfiles and all tasks applied.

@TechDufus TechDufus left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review notes (zsh-only scope; pacman + macOS only by design).

Fix before merge

  • Homebrew prefix is hardcoded to /opt/homebrew and breaks Intel Macs (/usr/local). Use $(brew --prefix):
    • bin/dotfiles:67 eval "$(/opt/homebrew/bin/brew shellenv)" fails under set -e when brew isn't there
    • modules/zsh/mise.macos.toml:5 login_shell = "/opt/homebrew/bin/zsh"
    • modules/clangd/mise.macos.toml:5 clangd source /opt/homebrew/opt/llvm/bin/clangd

Docs

  • README: note that only omarchy/cachyos/macos are supported now (bin/dotfiles:36-59) and that Ubuntu/Fedora/etc. Ansible support is recoverable from history.
  • Migration notes for existing Ansible-managed machines: old links/copies, the conf.d backup behavior (bin/dotfiles:89-98), and version-manager cleanup.

Leftovers (suggestions)

  • modules/bash/bashrc:110 still loads the oh-my-bash goenv plugin.
  • modules/zsh/zsh/paths_vars.zsh:18-20 rbenv / Homebrew ruby PATH branch.
  • modules/zsh/p10k.zsh:53 nvm prompt segment; consider removing it or swapping in a mise/asdf segment.
  • Nothing removes ~/.nvm, ~/.pyenv, ~/.asdf; consider a one-time cleanup task or a README note so they can't shadow mise.

Tests / CI

  • .github/workflows/mise.yml only dry-runs. Consider a job that applies for real and then runs a second time to prove idempotency, plus a zsh smoke test (e.g. zsh -ic 'command -v node').
  • bin/tests/test_dotfiles_requirements.sh was deleted with nothing replacing that coverage.

Verify

  • Check min_version = "2026.10.6" (under [bootstrap.brew] in modules/base/mise.macos.toml) and the mise features this relies on (mise bootstrap, [dotfiles], [bootstrap.packages], system_packages.managers) against the mise docs.

Optional

  • mise activate zsh is only needed if you want per-folder [env]; shims (paths_vars.zsh:14) are fine otherwise.

mise's brew manager skips every entry on Intel Macs and Homebrew's
installer refuses them, so stop early with "unsupported machine
(Intel Mac)". Run brew shellenv only when /opt/homebrew/bin/brew
exists.

test:base runs the real script in a sandbox (env -i, temp HOME and
XDG dirs, stub mise/uname/brew) and covers profile detection, the
conf.d backup and link, miserc rewrites and the pull rules.
DOTFILES_SYSROOT lets it fake /etc/os-release and /usr/share/omarchy.

check now runs from home. Inside modules/base, mise also loads that
file as a project config whose tasks run from ~, so the nested
`mise run 'test:*'` could not find test:base's script.
Dry-runs missed both run-stopping bugs of the migration: the npm
trust-policy failure in the tools step and a failed package install.
The apply job runs the dotfiles and tools steps for real in an Arch
container (cachyos profile) three times. The second run settles
permissions on files the first run's merges create; the third must
change nothing. Then zsh must find the mise shims, interactive or not.

Packages, services and system files stay dry-run only: a stock
container has no systemd, AUR helper, multilib or Omarchy repo.
go tools need gcc for cgo, and registry lookups get 120s instead of
the 20s default. About 3 minutes, in parallel with the arch job.
Other systems stop with "unsupported machine"; the Ansible version,
which also covered Ubuntu, Fedora and WSL, is tagged ansible-final.
macOS means Apple Silicon, and an existing conf.d is moved aside.
mise manages every runtime now; no goenv or rbenv is installed. The
oh-my-bash ansible plugin stays: it only adds aliases for the Ansible
CLI, which is still in use.
Replace the 1,669-line wizard config with a hand-written one that
looks like Tern's "Tern" preset in other terminals: rounded chips for
the directory (…/ plus the last three parts, the last in bold) and the
git status, the clock on the right, ❯ on its own line, and earlier
prompts collapsed to ❯. Colors are Catppuccin Mocha; each chip's
background is its text color at about 14% over the base, as sampled
from Tern's preset.

Kept: the git counters (⇣⇡ *~+!?), the status, duration, background
job, direnv, virtualenv, kubecontext, terraform, aws and context
segments with their show-on-command rules, transient prompt and quiet
instant prompt. Dropped: os_icon with its CachyOS override, and the
dead nvm segment.

Inside Tern its own preset still draws the prompt; Tern's "Your p10k
config" option would draw these chips with flat edges.
A mise older than the module-folder support loads no module, never
reads min_version, and `mise bootstrap` exits 0 having done nothing.
The first macOS CI run did exactly that with mise 2026.9.1.

Read min_version from modules/base/mise.toml and compare it with
`mise --version` field by field before linking anything. On macOS try
`brew upgrade mise` once; on Linux stop with the upgrade hint. Never
run pacman -Sy here: that is a partial upgrade, and Omarchy blocks it.
The macos job passed while checking nothing. The runner image sets
HOMEBREW_NO_AUTO_UPDATE=1, so brew poured a stale mise 2026.9.1, which
ignores the module folders. Run brew update first, and in every job
check that the base module's check task exists right after linking
conf.d.

Also move actions/checkout and actions/setup-node to v7 (Node 24);
v4 ran on the deprecated Node 20 runtime.
In a terminal, dotfiles now runs mise bootstrap one stage at a time
(packages, system, dotfiles, shell, tools, modules) behind a Catppuccin
summary. Each stage shows a spinner with the live log line, then its
result: counts from mise's JSON when jq is present, fail-soft module
warnings, or a box with the log tail when it fails. mise's full output
goes to ~/.local/state/dotfiles/last-run.log.

It asks for sudo up front, applies the dotfiles a second time when the
first pass created files that permission entries need, and runs the
login-shell stage on the terminal when chsh will prompt.

Piped output, --plain, -v, NO_COLOR and flags the UI does not handle
keep the old behaviour: one `mise bootstrap --yes` with mise's output.
color_theme only styles mise's interactive prompts, such as the task
picker in `mise run`; it matches the dotfiles summary and the prompt.
A fresh Omarchy ships its own ghostty, kitty and nvim configs, and omp
leaves its own files too. mise refuses to replace a real file or
directory with a link, so the first run stopped with "refusing to
overwrite existing files" until it was forced.

Before bootstrapping, ask mise which targets it would refuse
(`mise dot apply --dry-run` lists them) and move each one to
<path>.pre-dotfiles.<epoch>, as conf.d already is and as the Ansible
version kept backups. A dry run only reports them and previews with
--force-dotfiles, which writes nothing then. Runs that leave out the
dotfiles part move nothing.
The repo config merges into Omarchy's herdr config, which sets
prefix = "ctrl+space". The Ansible copy never set a prefix, so
herdr's ctrl+b default applied; pin it explicitly. herdr 0.9.1
rejects a prefix array, so ctrl+space cannot be kept alongside.
Mirrors the Hammerspoon GrokBot binding (summon = 'G'). Matches the
grok-bot window class and launches /usr/bin/grok-bot.
Omarchy's ~/.local/bin wrappers for codex, claude, omp and opencode
run `mise use -g`. A second global declaration in conf.d made omp and
codex fail ("declared in multiple global config files") and made
claude/opencode pin versions into the repo's mise.toml. Declare these
tools only on macOS and CachyOS.
Tern manages the omp version; update manually with `omp update`
outside Tern.
Every cell now floats: windows sharing a cell overlap (focused on
top, inset by Hyprland gaps/border) and nothing reflows on open or
close. Summon and Super+U raise the window they place.

- Hyper+P / Hyper+; / Hyper+' pick, cycle, reset the screen layout
- lilHyper+O / Hyper+O move the window to the next/previous screen
- Super+L toggles a workspace between cells and full screen only
- Super+U and screen layout choices persist in
  $XDG_STATE_HOME/dotfiles/cells.lua across logouts

Verified live on eDP-1: cell geometry, stacking, Super+U persistence,
Super+L round trip, reset back to the Fullscreen default.
Ctrl+] / Ctrl+[ move to the next / previous block, and
Ctrl+Shift+] / Ctrl+Shift+[ to the next / previous tab across
sessions. A second merge entry owns only these chords in Tern's
settings.json, so other bindings stay per machine. macOS keeps
Tern's defaults for now.
Standard Dev puts the browser left and terminals right; both need the
same workspace for the cells to show side by side.
Ctrl+Shift+[ misbehaved on Omarchy, so next / previous tab across
sessions moves to Ctrl+Alt+] / Ctrl+Alt+[. Ctrl+Shift+] / [ fall
back to Tern's default next / previous block. Ctrl+Alt+[ / ] were
only editor fold / unfold in Tern and are unbound in Hyprland.
btop reads AMD GPU stats from librocm_smi64.so and drops a gpu0 box it
cannot read. Install rocm-smi-lib on the Omarchy profile and own
shown_boxes with gpu0 there; the integrated Renoir Radeon shows up.
Keep Discord personal-only on macOS and Linux without changing shared
apps or OS profiles. Persist purpose locally through config-only setters.

Verify native package selection, preview safety and idempotence. Fix the
macOS Zsh fixture's temporary-path alias without weakening cache guards.
Manage surface_chat = "Console" in the shared settings merge and stop
bootstrap.sh from deleting surface_chat; it now only drops the legacy
opacity override. Console differs from Tern's Reader default, so Tern
keeps it when it saves.

Verified: tests/test_bootstrap.sh; isolated-HOME mise dot apply plus
bootstrap leaves surface_chat=Console and removes opacity.
Super+O / Super+Shift+O now move the focused window to the next /
previous screen into its remembered cell, as Command+O did under
Hammerspoon and Meta+O under Plasma. They replace Omarchy's pop-out
and Obsidian bindings.

- Super+U cell choices are remembered per screen and layout; the
  older layout-only choices are dropped on load.
- Floats cells does not own keep their relative box across screens;
  fullscreen windows stay fullscreen and return to their cell after.
- screens entries can match by description; the HP P224 defaults to
  Fullscreen.
- hypr/monitors.lua sets up the 4K + 1080p desk: arrangement, login
  cursor on the 4K, and workspaces 1-3 / 4-5 bound per screen only
  while both panels are connected (EDID check), so laptops keep
  starting on workspace 1.

Verified: all test:* suites pass; hyprctl reload reports no config
errors; live round trips on both screens (cell, Super+U memory,
native float, fullscreen).
The desk's external keyboard sends F13 (keycode 191) from its Caps
Lock key, so the CapsLock-only leader never fired on it. F13 is the
key Karabiner produced on macOS and keyd also took on CachyOS.

Verified: test:omarchy summon suite; live, Caps Lock then b on the
external keyboard summoned the browser.
Super+O asks for a window's place on the other screen, as Command+O
always snapped there under Hammerspoon, so a window tiled with
Super+T (or by a float toggle) goes back to its cell, like Super+U.
README documents this and the F13 leader.

Verified: new cells case fails without the change; live, a tiled
window moved to the HP and back landed in its cell.
Clone iamanro/omarchy-omp-usage and robzolkos/omarchy-github into
~/.config/omarchy/plugins via bootstrap.repos, like the themes.
bootstrap:omarchy-plugins validates them, puts them in the bar and
takes the stock omarchy.agents widget out through the running shell,
once per plugin list, so later bar edits stay. Outside a session or
on any failure it skips without recording, and retries next run.

Verified: test:* suites pass; live run cloned both, enabled them
after omarchy.tray, removed omarchy.agents; a rerun was a no-op.
The plugins run unsandboxed inside omarchy-shell, so dotfiles --update
must not fast-forward them to unreviewed code. Pin each repo to the
commit whose source was reviewed; upgrading is omarchy plugin update
(diff + validation) followed by bumping ref.

Verified: repos status shows both current at the pinned SHAs; repos
update --dry-run fetches only the unpinned theme.
Pinning meant hand-bumping commit SHAs, and every dotfiles run
reverted omarchy plugin update. Track upstream like the themes:
dotfiles --update fast-forwards both plugins.

This reverts the pinning from 6356e21; the files now match eb62669.
Verified: repos status current, both checkouts on main, and repos
update --dry-run fetches and fast-forwards both plugins.
omarchy update never touches third-party plugins, and unpinned repos
only moved on dotfiles --update. Track each plugin's main branch so
every dotfiles run fast-forwards it to the newest commit.

Verified: repos status shows both on main and current; repos apply
--dry-run reports nothing to change.
Tracking main made every dotfiles run fetch the plugins; offline,
mise bootstrap repos apply exits 1 and aborts the whole bootstrap,
which breaks the fail-soft rule. Unpin the repos again and link a
post-update hook that runs omarchy plugin update --yes, so every
omarchy update brings git-managed plugins to latest (validated,
rolled back on failure). dotfiles --update still fast-forwards them.

Verified: offline repos apply now exits 0; with omp-usage rewound
one commit, the linked hook fast-forwarded it back to main and both
plugins stayed in the bar; test:omarchy passes.
Omarchy dims windows to 0.985 / 0.96 and unfocused browsers to
0.985, so video showed the wallpaper through. A catch-all rule loaded
after Omarchy's window rules sets 1 / 1; apps that want translucency
set it themselves (Ghostty background-opacity).

Verified: hyprctl reload with empty configerrors; getprop reports
opacity 1 and opacity_inactive 1 for every open window (Brave was
1 / 0.985, the rest 0.985 / 0.96).
Standard Dev, 4K and HD layouts get a last full_screen cell, like
Hammerspoon's empty full-screen slot, so Super+U can make one app
fill the screen (per screen and layout) without switching layouts.

Verified: test:omarchy suites; live, a window assigned to the cell
filled the 4K's usable area below the bar and reset back.
Linux deployed no k9s config, so k9s ran on its generated defaults
and the stock skin. Link config, plugins and aliases on Linux too.

On Omarchy, k9s.yaml.tpl renders with each theme change and a
theme-set hook copies it to skins/current.yaml; reactive k9s
sessions retint live. macOS and CachyOS link catppuccin_mocha there.

Turn the header and logo back on, drop the config keys the v0.51
schema rejects, use a real shellPod image, and make catppuccin
crumbs and dialog buttons readable.
Headless theme switches skip theme-set hooks, and the task's
sources cache kept it from rerunning after the first-run theme
switch, so a fresh or SSH bootstrap left skins/current.yaml absent.

Run bootstrap:k9s every time after bootstrap:omarchy-theme; refresh
the theme only when the rendered skin is missing or older than the
template, and copy it only when it differs.
Hyprland 0.56 swaps a workspace's tiled layout on config reloads and
layout switches and drops its fullscreen state without unsetting the
app's xdg fullscreen flag. Brave then re-entered browser fullscreen
(hidden tabs) on every focus change. Cells now records each window's
fullscreen mode and restores it after props refresh.

Verified live: fullscreen Brave survived hyprctl reload at 2/2, F11
exit cleared the record, and focus changes kept the tabs.
Adds a regression case for the reload fullscreen re-sync: a mode
Hyprland dropped silently is restored (fullscreen and maximized), a
window the app took out of fullscreen is not. README and comment now
claim only the verified trigger (config reloads); layout switches are
source-backed, not exercised.
The omp-cloud plugin reads its connection from omp settings. The
token is read from 1Password (HomeLab/omp-cloud) at first use, so no
secret lives in this file. The herdr integrations stay local.
omp's /cloud prefers OMP_CLOUD_TOKEN over the op read in
omp/config.yml, so a shell with a refreshed secret cache hands off
without a 1Password prompt. The read joins wave one and adds no op
call.
Clone piyush97/omaswitch with the other shell plugins and enable it.
plugins.sh now enables any plugin kind (OMARCHY_PLUGINS), not only
bar widgets; an overlay is enabled through shell.json plugins[].

Alt+Tab opens OmaSwitch while the plugin is installed and enabled,
checked on each press, and otherwise keeps cells' window cycling, so
disabling or removing the plugin needs no keybinding change.
The HomeLab/OpenMonitor 1Password item no longer exists. Its reads
failed, and the all-or-nothing recipe aborted every refresh, leaving
no secret cache. Remove the OTEL key, endpoint, and header exports
along with the OTLP protocol defaults that only served them.
bootstrap:omp adds the TechDufus/omp-cloud marketplace and installs
omp-cloud@omp-cloud when omp does not already have it, either from a
marketplace or as a linked checkout (which shares node_modules/omp-cloud).
The marketplace is added first because an unconfigured name@marketplace
is treated as an npm spec. Upgrades stay manual: omp plugin upgrade.
Switch cloud.url from the localhost default to
omp-cloud.home.techdufus.com:8080 so sessions on any machine reach the
shared service. Verified by delegating and messaging cloud thread
vavgqez05fnt from this session through that endpoint.
Restructure the README after high-star rice and terminal-app repos
(Hyprland, HyDE, end-4, codex, mise, atuin): dark/light wordmark
header, Catppuccin for-the-badge shields, kbd nav, framed Omarchy
hero, gallery, collapsed operational notes, star history, license.
All existing text is kept.

.github/assets/render/render.py regenerates every image: the real
bin/dotfiles UI against a stub mise and sudo in a sandbox HOME,
captured from a private tmux server and rasterized with rsvg-convert.
Agent instruction files listed live config (bindings, package names,
file paths, plugin inventories) that drifts with every change and
already disagreed with the README. Keep the principles and boundaries;
the config and README are the source of truth for specifics.
@TechDufus
TechDufus marked this pull request as ready for review October 23, 2026 01:43
@TechDufus
TechDufus merged commit c7be02a into main Oct 23, 2026
2 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant