dev_env: install Node.js from pinned tarball, drop npm bloat

Debian's npm package pulls a ~400-package node-* tree (the first deploy
installed 527 packages). Replace apt nodejs+npm with a pinned upstream Node
tarball (v20.19.2) installed to /opt + symlinked, mirroring the nvim install
pattern (ADR-014 pinning). npm/npx come bundled. Molecule verifies node/npm
on PATH; lint + idempotent converge green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sjat 2026-06-11 14:21:33 +02:00
parent 6203513220
commit aea4f8c3d6
5 changed files with 78 additions and 15 deletions

View file

@ -10,11 +10,12 @@ or service VMs.
## What it does
- Installs packages: `zsh, tmux, git, stow, build-essential, curl, ca-certificates,
fzf, ripgrep, direnv, nodejs, npm` (`dev_env__packages`).
- Installs **pinned** neovim (`dev_env__nvim_version`) and oh-my-posh
(`dev_env__omp_version`) from GitHub releases, and the system-wide oh-my-posh theme
`/etc/oh-my-posh/zen.toml`.
- Installs packages: `zsh, tmux, git, stow, acl, build-essential, curl,
ca-certificates, fzf, ripgrep, direnv` (`dev_env__packages`).
- Installs **pinned** neovim (`dev_env__nvim_version`), oh-my-posh
(`dev_env__omp_version`) and Node.js (`dev_env__node_version`) from upstream releases
(Node from the nodejs.org tarball — not Debian's `npm`, which pulls a ~400-package
tree), plus the system-wide oh-my-posh theme `/etc/oh-my-posh/zen.toml`.
- For each user in `dev_env__users`: sets the login shell to zsh, clones oh-my-zsh +
custom plugins and the tmux/TPM plugins, and **stows** the dotfiles into `~`.
@ -32,6 +33,7 @@ LSPs/formatters self-install via mason (no system LSP packages needed).
| `dev_env__users` | `[]` | Users to configure. Set per group, e.g. `group_vars/control → [sjat, claude]`. Empty = no per-user work. |
| `dev_env__nvim_version` | `v0.12.2` | Pinned neovim release. |
| `dev_env__omp_version` | `29.0.1` | Pinned oh-my-posh release. |
| `dev_env__node_version` | `v20.19.2` | Pinned Node.js release (nodejs.org tarball; npm bundled). |
| `dev_env__packages` | see defaults | APT packages. |
| `dev_env__omz_custom_plugins` | autosuggestions, syntax-highlighting | Cloned into `~/.oh-my-zsh/custom/plugins`. |
| `dev_env__tmux_plugins` | tpm, tmux-sensible, vim-tmux-navigator, catppuccin@v1.0.3 | Cloned into `~/.tmux/plugins`. |

View file

@ -6,27 +6,28 @@
# workstation-class group (e.g. group_vars/control → [sjat, claude]).
dev_env__users: []
# APT packages. nvim + oh-my-posh are installed separately from pinned releases.
# (nvim uses mason internally for LSPs, so no system LSP packages are needed; node is
# present so mason's node-based servers work. direnv is referenced by the .zshrc.)
# APT packages. nvim, oh-my-posh and Node.js are installed separately from pinned
# releases — Debian's `npm` pulls a ~400-package node-* tree, so we use the upstream
# Node tarball instead (npm bundled). nvim uses mason internally for LSPs; node is
# present so mason's node-based servers work. direnv is referenced by the .zshrc;
# acl lets Ansible become_user an unprivileged user (sjat -> claude) for file copies.
dev_env__packages:
- zsh
- tmux
- git
- stow
- acl # lets Ansible become_user an unprivileged user (sjat -> claude) for file copies
- acl
- build-essential
- curl
- ca-certificates
- fzf
- ripgrep
- direnv
- nodejs
- npm
# Pinned tool versions (ADR-014 — pin, don't track "latest").
dev_env__nvim_version: "v0.12.2"
dev_env__omp_version: "29.0.1"
dev_env__node_version: "v20.19.2"
# oh-my-zsh custom plugins (cloned per user into ~/.oh-my-zsh/custom/plugins).
dev_env__omz_custom_plugins:

View file

@ -23,6 +23,8 @@
loop:
- /usr/local/bin/nvim
- /usr/local/bin/oh-my-posh
- /usr/local/bin/node
- /usr/local/bin/npm
- /etc/oh-my-posh/zen.toml
register: dev_env__sys
loop_control:
@ -31,10 +33,8 @@
- name: Assert system tools are installed
ansible.builtin.assert:
that:
- dev_env__sys.results[0].stat.exists
- dev_env__sys.results[1].stat.exists
- dev_env__sys.results[2].stat.exists
fail_msg: nvim/oh-my-posh/zen.toml missing
- dev_env__sys.results | map(attribute='stat.exists') | min
fail_msg: a system tool (nvim/oh-my-posh/node/npm/zen.toml) is missing
- name: Look up the test user
ansible.builtin.getent:

View file

@ -15,6 +15,10 @@
ansible.builtin.include_tasks: oh_my_posh.yml
tags: [packages]
- name: Install Node.js (pinned release)
ansible.builtin.include_tasks: nodejs.yml
tags: [packages]
- name: Configure each developer user
ansible.builtin.include_tasks: per_user.yml
loop: "{{ dev_env__users }}"

View file

@ -0,0 +1,56 @@
---
- name: Node.js | Read installed-version sentinel
ansible.builtin.slurp:
src: /etc/node_installed_version
register: dev_env__node_sentinel
failed_when: false
- name: Node.js | Determine installed version
ansible.builtin.set_fact:
dev_env__node_installed: >-
{{ (dev_env__node_sentinel.content | b64decode | trim)
if dev_env__node_sentinel.content is defined else '' }}
- name: Node.js | Install pinned release
when: dev_env__node_installed != dev_env__node_version
block:
- name: Node.js | Download release tarball
ansible.builtin.get_url:
url: "https://nodejs.org/dist/{{ dev_env__node_version }}/node-{{ dev_env__node_version }}-linux-x64.tar.gz"
dest: "/tmp/node-{{ dev_env__node_version }}.tar.gz"
mode: "0644"
- name: Node.js | Create versioned install directory
ansible.builtin.file:
path: "/opt/node-{{ dev_env__node_version }}"
state: directory
mode: "0755"
- name: Node.js | Extract tarball
ansible.builtin.unarchive:
src: "/tmp/node-{{ dev_env__node_version }}.tar.gz"
dest: "/opt/node-{{ dev_env__node_version }}"
remote_src: true
extra_opts: ["--strip-components=1"]
- name: Node.js | Symlink node, npm, npx into PATH
ansible.builtin.file:
src: "/opt/node-{{ dev_env__node_version }}/bin/{{ item }}"
dest: "/usr/local/bin/{{ item }}"
state: link
force: true
loop:
- node
- npm
- npx
- name: Node.js | Write version sentinel
ansible.builtin.copy:
content: "{{ dev_env__node_version }}"
dest: /etc/node_installed_version
mode: "0644"
- name: Node.js | Remove downloaded tarball
ansible.builtin.file:
path: "/tmp/node-{{ dev_env__node_version }}.tar.gz"
state: absent