Idempotent, environment-aware machine setup for ephemeral dev VMs and real
dev machines. Peer repo to dotfiles —
this repo installs tools; the dotfiles repo manages config via its
existing bare-repo (dot) workflow, which this repo invokes at the end.
Re-running is fast and idempotent: by default it installs only what's
missing and skips everything already present. To bump already-installed tools to
their latest versions, run the upgrade path (update-env.sh --upgrade).
On a fresh machine:
-
Set your git identity for the dotfiles bare repo via environment variables — no need to edit or fork this repo:
export DOTFILES_USER_NAME="Your Name" export DOTFILES_USER_EMAIL="you@example.com"
(Optional:
export DOTFILES_REPO_URL=...to use your own dotfiles fork.) These are read at run time; if unset, the fallbacks ingroup_vars/all.ymlare used. -
Run the bootstrap in the same shell (exported vars carry through):
# remote one-liner: curl -fsSL https://raw.githubusercontent.com/jsco2t/devbox-provision/main/bootstrap.sh | bash # or from a local clone: git clone https://github.com/jsco2t/devbox-provision.git && cd devbox-provision ./update-env.sh
bootstrap.sh ensures git is present, asks where to clone the repo (default:
<current dir>/devbox-provision), clones it, then hands off to the repo's
update-env.sh. That installs Ansible and converges everything: native
packages, Homebrew tools, Go/Rust toolchains, the source-built oom-edit
Markdown editor, the Helix language tooling, Git Credential Manager, and finally
your dotfiles. It also drops a ~/update-env.sh wrapper so you can re-converge
any time with a single command.
See Usage for re-runs, upgrades, dry runs, and details.
- OS: Linux and macOS (no Windows)
- Linux families: Debian-based and Enterprise Linux (RedHat family)
- Arch: arm64 and x86_64 (Homebrew, the native package managers, rustup, and
the language installers resolve arch themselves; the
git_credential_managerrole maps it to GCM'sx64/arm64release assets) - CI coverage: CI runs on Debian only. Enterprise Linux 9/10 and macOS are supported by design but not tested in CI.
bootstrap.sh clones this repo locally; update-env.sh then installs Ansible
and runs local.yml against localhost (connection: local) — the same path
CI exercises. Roles run in order:
- common — print environment summary, ensure git / Xcode CLT
- native_packages — low-level utils from
apt/dnf(enables EPEL on EL) - homebrew — install Homebrew (Linux + Mac), then fast-moving tools +
the
node/uvtoolchains - desktop_tools — JetBrains Mono Nerd Font and Kitty Linux installation /
desktop integration (their macOS casks are handled by
homebrew) - golang — install Go via Homebrew
- rust — install Rust via
rustup+ therust-analyzercomponent - oom_edit — clone
oom-edit, build its release target, and link it from~/.local/bin - lang_tools — Helix editor LSPs/formatters/linters via their native
installers (
go install,cargo install,npm i -g,uv tool install) - git_credential_manager — install Git Credential Manager (GCM) from its
GitHub release tarball into
~/.local/share/gcm-core, linked from~/.local/bin; it never configures GCM - dotfiles — reproduce the bare-repo
dotworkflow idempotently
Environment dispatch uses Ansible facts, not hand-rolled detection:
| Fact | Drives |
|---|---|
ansible_facts['system'] (Linux/Darwin) |
brew prefix, native-vs-brew split, GCM linux/osx asset |
ansible_facts['os_family'] (Debian/RedHat) |
apt vs dnf, EPEL, ICU package for GCM |
ansible_facts['architecture'] |
Homebrew prefix (/opt/homebrew vs /usr/local), GCM x64/arm64 asset |
- Native (
apt/dnf): low-level utils that rarely change —jq,fzf,ripgrep,fd,vim,tmux, git, build tooling. Installedstate: presentby default (state: latestunder--upgrade). Seeroles/native_packages/vars/main.yml. - Homebrew (Linux + Mac): fast-moving tools (
bat,yq,neovim,helix,starship,eza,zoxide,git-delta,lazygit,gh), the brew-only LSP tools (marksman,shellcheck), and thenode/uvtoolchains. Seeroles/homebrew/vars/main.yml. - Desktop tools: Kitty and JetBrains Mono Nerd Font use Homebrew casks on macOS. Linux uses their official upstream distributions; Kitty receives a per-user application entry when KDE/Plasma or COSMIC is detected.
- Go (
go install): Go LSP/formatter/linter set —gopls,dlv,goimports,gofumpt,golangci-lint,staticcheck,yamlfmt,shfmt,efm-langserver,helm-ls, plusterraform-ls(built from source rather than HashiCorp's brew tap, to avoid that tap's trust gate). - Cargo (
cargo install):taplo-cli,harper-ls,dprint,markdown-oxide(git source). - npm (
npm i -g): language servers for Ansible, JSON/HTML/CSS, Dockerfile, Docker Compose, YAML, Bash, plusmarkdownlint-cliandprettier. - uv (
uv tool install):python-lsp-server,black. - Source build (
make build-release):oom-editis cloned to~/.local/bin/oom-edit-src; its release binary remains in that checkout and~/.local/bin/oom-editlinks to it. The role runs after rustup so the repository-pinned Rust toolchain can be resolved. - GitHub release tarball: Git Credential Manager, on Linux and macOS alike.
The Homebrew cask is avoided because its
.pkgneeds sudo and its postinstall runsgit-credential-manager configure, which rewrites~/.gitconfig. A default run installs the release pinned inroles/git_credential_manager/defaults/main.yml(gcm_versionplus a sha256 per asset), so it makes no GitHub API call;--upgraderesolves the latest release and its sha256 from the GitHub API. The download is verified against that sha256, extracted to~/.local/share/gcm-core/<version>/, and linked as~/.local/bin/git-credential-manager. On Linux the role also installs the ICU runtime GCM needs (libicuon EL,libicuNNon Debian).
The lang_tools lists live in roles/lang_tools/vars/main.yml. They mirror
what jsco2t/dotfiles's .config/helix/deps.sh installs, expressed as
idempotent Ansible. Which hosts use GCM, its credential store, and the
precedence that keeps gh serving github.com are configuration owned by
jsco2t/dotfiles (.config/git/config); this repository only installs GCM.
Yazi's Markdown opener likewise remains configuration owned by the
peer jsco2t/dotfiles repository; this repository only installs the tool. That
configuration routes Markdown through ~/.local/bin/md_router.sh, which uses
the adjacent oom-edit when available and falls back to hx otherwise.
curl -fsSL https://raw.githubusercontent.com/jsco2t/devbox-provision/main/bootstrap.sh | bashEnsures git, clones the repo to a location you choose (default
<cwd>/devbox-provision), then runs update-env.sh, which installs Ansible +
the community.general collection and converges the machine. Set
PROVISION_DEST=/path/to/dir to skip the interactive clone-location prompt.
After the first run there is a ~/update-env.sh wrapper (it cds into the
clone and re-runs its update-env.sh):
~/update-env.sh # fast converge: install only what's missing
~/update-env.sh --upgrade # bump every tool to its latest version
~/update-env.sh --check # dry run (--check --diff)Equivalently, from the clone itself: ./update-env.sh [--upgrade|--check].
Default vs. upgrade. A default run is fast and idempotent — it installs
missing tools and skips everything already present (go/cargo/npm/uv
install tasks and the oom-edit source build are guarded on the resulting
binary, and rustup update runs only before building a missing cargo tool;
Homebrew and apt/dnf use state: present and skip brew update; GCM
is installed only when ~/.local/bin/git-credential-manager does not resolve to
a binary). --upgrade is the slow path: it runs brew update, state: latest,
re-fetches the language tools at @latest, runs rustup update, pulls the
configured oom-edit branch, invokes make build-release again, and
re-downloads the latest GCM release. Set GITHUB_TOKEN to authenticate the GCM
upgrade's GitHub API call; without it the unauthenticated rate limit applies.
Bumping the pinned GCM release changes only fresh installs: hosts that already
have GCM move to a new version on --upgrade. To bump the pin, update
gcm_version and gcm_sha256 from the tag_name and per-asset digest at
https://api.github.com/repos/git-ecosystem/git-credential-manager/releases/latest,
and update the GCM_EXPECTED_VERSION default in
roles/git_credential_manager/tests/verify.sh to match.
~/update-env.sh --check
# or directly:
ansible-playbook -i 'localhost,' -c local local.yml --check --diffYour git identity is written to the dotfiles bare repo via config --local
(exactly as the original .dotsetup.sh did; it does not touch global git
config). Two ways to set it:
Environment variables (no repo edit needed):
export DOTFILES_USER_NAME="Your Name"
export DOTFILES_USER_EMAIL="you@example.com"
export DOTFILES_REPO_URL="https://github.com/you/dotfiles.git" # optionalThese are read at run time by group_vars/all.yml. If a variable is unset or
empty, the fallback baked into group_vars/all.yml is used.
Or edit the fallbacks in group_vars/all.yml if you own/fork this repo:
dotfiles_user_name: "{{ lookup('env', 'DOTFILES_USER_NAME') | default('Jason', true) }}"
dotfiles_user_email: "{{ lookup('env', 'DOTFILES_USER_EMAIL') | default('you@example.com', true) }}".github/workflows/ci.yml runs on every push to main (and on demand via
workflow_dispatch). It spins up a Debian (debian:trixie) container, creates
an unprivileged user, and runs update-env.sh three times:
- setup — a default converge installs everything that's missing,
--upgrade— exercises the bump-to-latest path,- default again — asserts the steady-state converge reports
changed=0(the idempotency guarantee).
CI covers Debian only. The Enterprise Linux 9/10 and macOS paths are reasoned
about but not tested in CI. roles/git_credential_manager/tests/verify.sh is a
role-level acceptance test you can run by hand. It deletes and reinstalls GCM
on the host it runs on.
- The dotfiles role does not run the original destructive
rm -frpreamble. It skips the bare clone if~/.dotfilesalready exists and usesreset --hard FETCH_HEADto apply tracked files, leaving untracked files in$HOMEalone. - On Linux, ensure your dotfiles put the toolchains on
PATH:eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)", plus~/.cargo/bin,~/go/bin, and~/.local/bin(cargo / go / uv install targets).