Migrate from a hand-rolled bootstrap dotfiles repo
A huge number of dotfiles repos follow the layout Zach Holman popularized: a folder per topic, files ending in .symlink that a script links into $HOME, files ending in .zsh that get sourced on startup, a bin/ on your PATH, and a script/bootstrap that wires it all together. If your repo looks like that, or like one of the countless variations on it, this guide maps each piece onto Dotfiles Pro so you can keep the topic-by-topic mindset and delete the bootstrap glue.
Your bootstrap repo today
~/dotfiles/
git/
gitconfig.symlink
aliases.zsh
zsh/
path.zsh
prompt.zsh
bin/
my-script
homebrew/
install.sh
script/bootstrapThe bootstrap script does the work: it symlinks every *.symlink file into $HOME, sources every *.zsh file from your shell startup (often path.zsh first and completion.zsh last), runs each topic's install.sh, and puts bin/ on your PATH. Three jobs, file linking, shell loading, and installing, all carried by glue you wrote and maintain.
The mapping
Dotfiles Pro does those three jobs declaratively, so each convention has a home:
| Holman convention | What it did | Dotfiles Pro equivalent |
|---|---|---|
*.symlink (verbatim file) | symlinked into $HOME | the dotfiles role, link mode |
gitconfig.symlink and similar | owned the whole config file | a role's settings, e.g. git_config |
aliases.zsh | sourced aliases and functions | the aliases / functions vars |
path.zsh (PATH, env) | exported env on startup | a ~/.profile.d/ env snippet, or role vars |
prompt.zsh | configured the prompt | the starship role |
bin/ | added to PATH | ~/scripts (the unix role adds it to PATH) |
per-topic install.sh | installed the tool | a role, built-in or your own under roles/ |
script/bootstrap | ran the whole thing | the installer plus dotpro apply |
Step by step
Add the collection to your
requirements.yml:yamlcollections: - name: dotfilespro.rolesPromote settings off the symlinks. A
gitconfig.symlinkbecomesgit_config, so the role manages the keys you set and git keeps the rest:yaml- hosts: localhost # ... vars: git_config: user: name: Your Name email: you@example.com git_repositories: .dotfiles: repository: git@gitlab.com:yourname/mydotfiles.git path: ~/.dotfilesMove aliases and functions to vars. Your
aliases.zshcontent becomes data that renders into~/.profile.d/aliases/, the same loading behavior, without a sourced script:yamlfunctions: project: | tab "${1:-$(basename "$PWD")}" header "Project $(basename "$PWD")" kv Location "$PWD" kv Repository "$(gitrepo)" header 'Git status' git status aliases: production: "digitalocean acme && kubernetes acme" cloud: "cd /projects/acme/cloud/ && production && project 'Acme Cloud'"Link the rest as files. Anything genuinely verbatim (an
init.lua, atmux.conf, a shell snippet that is not aliases) stays a file, listed indotfiles_listfor thedotfilesrole to link, exactly what*.symlinkdid:yaml--- # playbook.yaml — the provisioning layer, at the repo root beside the raw dotfiles, # which stay usable on their own (a symlink, stow, or a plain copy). This playbook # owns everything around them: packages, system config, and the repositories to clone. - name: Provision this machine hosts: localhost connection: local gather_facts: true diff: true vars: dotpro_limit_host: false # default: true; single-host repo # The raw dotfiles to link into $HOME. The dotfiles role symlinks each one from # the repo root (dotfiles_dir defaults to dotpro_path), so there is no inline task # to write: the files stay the source of truth, the role links them. dotfiles_list: # default: [] - .bashrc - .gitconfig - .config/starship.toml roles: # OS base: the matching one runs, the other is skipped (facts decide) - role: dotfilespro.roles.ubuntu when: ansible_facts.os_family == 'Debian' - role: dotfilespro.roles.macos when: ansible_facts.os_family == 'Darwin' - dotfilespro.roles.dotpro - dotfilespro.roles.dotfiles - dotfilespro.roles.git - dotfilespro.roles.dockerKeep your scripts. Put the contents of
bin/in~/scripts(theunixrole creates it and adds it toPATH), or link yourbin/there with thedotfilesrole.Turn each
install.shinto a role. If a built-in role already installs the tool, list it. If not, drop a small role underroles/in your repo and the playbook picks it up by name.Apply, preview first, then delete
script/bootstrap:bashdotpro apply # install + configure this machine ansible-playbook playbook.yaml --check --diff # or a dry run that changes nothing
What the bootstrap script did, now built in
The reason to keep a bootstrap script was orchestration: link, then load, then install, in the right order, and ideally without breaking on a second run. The apply gives you that for free. It is idempotent, so re-running only changes what drifted, and a play already orders its work (pre_tasks, then roles, then tasks), so you express ordering as structure rather than as careful lines in a shell script. The --diff preview shows what a run would do before it does it, which no bootstrap ever did.
Keep the topic mindset
The part of the Holman layout worth keeping is the instinct to organize by tool, and you still do. A topic that is just config becomes a few role vars; a topic that installs something becomes a role; your scripts stay scripts. What goes away is the hand-written glue that linked, sourced, and installed, replaced by one reviewed declaration that brings the packages along with the files.
Next steps
- dotfiles role link, copy, or template modes, backup, and pruning
- Roles what each built-in role installs, and how to add your own
- Architecture how aliases, functions, and the
~/.profile.d/framework are assembled - Manage your dotfiles across machines the backup, restore, and sync workflow end to end