This is an Ansible-based Windows dotfile and workstation provisioning project.
It manages a single Windows machine (or multiple) from a macOS/Linux control node via WinRM,
or locally via WSL2. It is a ground-up rewrite of the bare-git sync-windows pattern — see
/Users/div/Projects/sync-windows for the original.
Target machine profile: Windows 10/11 personal workstation. Primary user: Divit Mittal.
macOS (ansible-playbook) ──WinRM──▶ Windows machine
OR
WSL2 on Windows (local connection) ──▶ Windows machine
Connection is configured per-host in inventory/hosts.yml. Default is ansible_connection: local
(WSL2). For remote, switch to winrm and run scripts/bootstrap-winrm.ps1 first on the target.
bootstrap ←── scoop ←── msys2
(no dep) winget
windows_settings
dotfiles ←── windows_settings
meta/main.yml in each role declares these. Ansible resolves them automatically — do not manually
re-include dependent roles in playbooks.
host_vars/windows_workstation.yml
group_vars/windows/packages.yml
group_vars/windows/main.yml
group_vars/all/vault.yml ← encrypted, contains git_email / Windows creds
group_vars/all/main.yml
role defaults/main.yml
Never read vault_* variables directly in tasks. They are aliased to clean names in
group_vars/windows/main.yml (e.g. git_email: "{{ vault_git_email }}").
| File | Purpose |
|---|---|
ansible.cfg |
WinRM transport, fact caching (1h TTL), yaml callback, vault path |
inventory/hosts.yml |
Host definitions — edit ansible_host for remote targets |
inventory/group_vars/windows/packages.yml |
All packages — Scoop, Winget, MSYS2 lists |
inventory/group_vars/windows/main.yml |
All Windows path variables (scoop_dir, appdata, etc.) |
inventory/group_vars/all/vault.yml |
Encrypted secrets — edit with make edit-vault |
inventory/host_vars/windows_workstation.yml |
Per-machine overrides (is_laptop, extra packages) |
Makefile |
Primary interface — make help lists all targets |
scripts/bootstrap-winrm.ps1 |
Run on Windows as Admin before first remote Ansible run |
scripts/install-ansible.sh |
Set up macOS control node (venv, collections, vault) |
- Sets PowerShell execution policy, NuGet/PSGallery trust
- Installs Scoop if absent (with
block/rescueerror handling) - Optionally configures WinRM (only when
bootstrap_configure_winrm: true) - Creates XDG base directories on the Windows target
- Adds buckets in order: main → extras → nerd-fonts → versions
- Uses
set_factto compute missing packages before looping — avoids shelling out for each block/rescuearound the install loop: collects failures, warns, does not abort
- Handles winget's non-standard exit code
-1978335189(already installed) infailed_when - Skips packages already present via string search on
winget listoutput
- Runs all pacman commands via
bash.exe -l -cto load the MSYS2 environment - Depends on
scoop(msys2 is installed as a Scoop app)
registry.yml: win_regedit for Developer Mode, telemetry, context menu, taskbar, power planenvironment.yml: win_environment for XDG vars, EDITOR, VISUAL, SHELL, GIT_CONFIG_GLOBALpath.yml: win_path for Scoop shims, .local/bin, MSYS2 bin — asserts PATH after updateexplorer.yml: Explorer advanced options loop (show extensions, hidden files, launch target)- Handlers:
restart explorer,refresh env— triggered vianotify/listen
- Static files (
roles/dotfiles/files/): deployed withwin_copy— wezterm Lua, tridactylrc, whkdrc, fastfetch config, git attributes/ignore - Jinja2 templates (
roles/dotfiles/templates/): deployed withwin_templategit/config.j2— injectsuser_name,git_email(from vault)Profile.ps1.j2— injects editor, shell, paths; feature flags gate MSYS2/starship sectionsstarship.toml.j2—is_laptopcontrols battery module;feature_msys2controls shell indicatorfirefox/user.js.j2— privacy hardening prefs, no per-user vars currently
- Firefox profile path is discovered at runtime via
win_find(not hardcoded)
All flags live in group_vars/all/main.yml. Set to false to skip entire subsystems:
feature_scoop: true/false
feature_winget: true/false
feature_msys2: true/false
feature_wezterm: true/false
feature_starship: true/false
feature_firefox: true/false
feature_tridactyl: true/false
feature_whkd: true/false
feature_kanata: false # legacy, off by default
feature_windows_settings: true/false
feature_git_config: true/falsePer-host variations (e.g. is_laptop: true) go in host_vars/<hostname>.yml.
ansible.windows >=2.0.0 — win_regedit, win_environment, win_path, win_service, etc.
community.windows >=2.0.0 — win_scoop (if used directly)
community.general >=9.0.0 — json_query filter, misc modules
community.crypto >=2.0.0 — vault helpers
Install: make deps or ansible-galaxy collection install -r requirements.yml -p .collections/
make deps # Install Python deps + Ansible collections
make bootstrap # Bootstrap fresh machine (WinRM + Scoop)
make dotfiles # Deploy configs only — fastest iteration
make packages # Install/ensure all packages
make update # Upgrade all packages (snapshots before/after)
make verify # Read-only state check
make site # Full run
make site-diff # Full run with --diff output
make edit-vault # Edit encrypted secrets
make lint # ansible-lint all playbooks
make check # Dry-run (--check --diff)Targeted runs with tags:
ansible-playbook playbooks/site.yml --tags git
ansible-playbook playbooks/site.yml --tags wezterm,starship
ansible-playbook playbooks/site.yml --skip-tags firefox
ansible-playbook playbooks/dotfiles.yml --diff- Open
inventory/group_vars/windows/packages.yml - Append to
scoop_packages,winget_packages, ormsys2_packages - Run
make packagesoransible-playbook playbooks/packages.yml --tags scoop
Static file:
- Drop it in
roles/dotfiles/files/<tool>/ - Add a
win_copytask inroles/dotfiles/tasks/configs.yml
Templated file:
- Create
roles/dotfiles/templates/<tool>/name.j2 - Add a
win_templatetask inroles/dotfiles/tasks/configs.yml - Expose any new variables in
group_vars/windows/main.yml(ordefaults/main.yml)
Open roles/windows_settings/tasks/registry.yml and add a win_regedit block.
All registry tasks are idempotent by default — win_regedit only writes when the value differs.
- Add a host entry in
inventory/hosts.ymlunderwindows: - Create
inventory/host_vars/<new_hostname>.ymlwith any overrides - Re-use all existing roles — they will run against both hosts in parallel (
forks = 10)
inventory/group_vars/all/vault.ymlis encrypted withansible-vault- Password stored in
.vault_pass(mode 600, gitignored) - Never commit
.vault_pass— it is in.gitignore - To rotate:
ansible-vault rekey inventory/group_vars/all/vault.yml - Variables inside:
vault_git_email,vault_git_signing_key,vault_windows_user,vault_windows_password
Every task is designed to be re-runnable with no side effects when state is already correct:
win_regedit— only writes on value changewin_environment/win_path— built-in idempotencywin_copy/win_template— MD5 comparison, only overwrites on diffwin_shellwithchanged_when— all shell tasks declare explicit change conditions- Scoop: pre-computes missing packages via
set_factbefore looping - Winget: string-match against
winget listoutput before attempting install
- No
git add -Ain this project — always stage specific files - LF line endings —
.editorconfigenforces this; do not change - 2-space indentation in YAML; role Lua files use 2-space as well
- Conventional commits:
feat(role):,fix(role):,chore(role):, etc. - Do not hardcode paths — use variables from
group_vars/windows/main.yml - Do not use
win_commandwhenwin_shellis needed for pipelines — and vice versa no_log: trueon any task that registers output containing credentials or email