From aea4f8c3d6910e9bf96f534b11b7613b97aa7f2d Mon Sep 17 00:00:00 2001 From: sjat Date: Thu, 11 Jun 2026 14:21:33 +0200 Subject: [PATCH] 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) --- roles/dev_env/README.md | 12 +++-- roles/dev_env/defaults/main.yml | 13 +++--- roles/dev_env/molecule/default/verify.yml | 8 ++-- roles/dev_env/tasks/main.yml | 4 ++ roles/dev_env/tasks/nodejs.yml | 56 +++++++++++++++++++++++ 5 files changed, 78 insertions(+), 15 deletions(-) create mode 100644 roles/dev_env/tasks/nodejs.yml diff --git a/roles/dev_env/README.md b/roles/dev_env/README.md index 070147c..8414697 100644 --- a/roles/dev_env/README.md +++ b/roles/dev_env/README.md @@ -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`. | diff --git a/roles/dev_env/defaults/main.yml b/roles/dev_env/defaults/main.yml index 5f6a982..70195a4 100644 --- a/roles/dev_env/defaults/main.yml +++ b/roles/dev_env/defaults/main.yml @@ -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: diff --git a/roles/dev_env/molecule/default/verify.yml b/roles/dev_env/molecule/default/verify.yml index b2a6600..04ac53a 100644 --- a/roles/dev_env/molecule/default/verify.yml +++ b/roles/dev_env/molecule/default/verify.yml @@ -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: diff --git a/roles/dev_env/tasks/main.yml b/roles/dev_env/tasks/main.yml index 1d11bf0..6e15ee6 100644 --- a/roles/dev_env/tasks/main.yml +++ b/roles/dev_env/tasks/main.yml @@ -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 }}" diff --git a/roles/dev_env/tasks/nodejs.yml b/roles/dev_env/tasks/nodejs.yml new file mode 100644 index 0000000..03b9b53 --- /dev/null +++ b/roles/dev_env/tasks/nodejs.yml @@ -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