Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ansible/roles/astral/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
# macOS : brew -- all three are core formulae
# Ubuntu : no apt repo from Astral. uv comes from the upstream tarball
# (user-owned in ~/.local/bin); ruff and ty install as uv tools
# (also user-owned). hyperi-update refreshes all three.
# (also user-owned). hyperi-update refreshes uv and uvx from the
# same release, since `uv self update` refuses a copy it did not
# install, and ruff and ty with `uv tool upgrade --all`.

# Own copy: meta-dependencies can run this role before any sibling has loaded
# theirs, and homebrew_env undefined aborts every macOS task below.
Expand Down
10 changes: 6 additions & 4 deletions ansible/roles/contributor/tasks/git_scrub.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
# failure: gitleaks scans FULL history, so a secret removed from HEAD still
# fails `hyperi-ci check`. soe inherits it through meta/dependencies.
#
# GitHub release tarball on every platform (Tier 3 -- hyperi-update pulls the
# latest on each run). It is the only rung: not on crates.io, no git-scrub path
# on downloads.hyperi.io, and the release's git-scrub.rb is not in the hyperi-io
# tap. Move macOS to community.general.homebrew once that formula is tapped.
# GitHub release tarball on every platform (Tier 3). hyperi-update pulls the
# latest on Linux. On macOS only a re-run of this role does, because the macOS
# updater leaves everything to brew. It is the only rung: not on crates.io, no
# git-scrub path on downloads.hyperi.io, and the release's git-scrub.rb is not
# in the hyperi-io tap. Move macOS to community.general.homebrew once that
# formula is tapped.
#
# The asset unpacks into a directory named after itself, so the extracted binary
# path carries the version. The tag has a leading `v`; the filename does not.
Expand Down
9 changes: 8 additions & 1 deletion ansible/roles/developer-go/tasks/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,10 @@
path: "/tmp/go{{ go_version }}.linux-{{ hyperi_arch_deb }}.tar.gz"
state: absent

# /etc/profile is re-read by every login shell, so the prepend must test
# /etc/profile is re-read by every login shell, so each entry must test
# PATH first or it stacks a copy per nested shell, per tmux pane, per `su -`.
# ~/go/bin is appended, not prepended, so a go-installed copy never shadows
# a packaged tool of the same name.
- name: Add the Go toolchain to PATH (system-wide)
ansible.builtin.copy:
content: |
Expand All @@ -99,6 +101,11 @@
*":/usr/local/go/bin:"*) ;;
*) export PATH="/usr/local/go/bin:$PATH" ;;
esac
# gopls, govulncheck and flarectl are go-installed here, and gosec on Ubuntu.
case ":$PATH:" in
*":$HOME/go/bin:"*) ;;
*) export PATH="$PATH:$HOME/go/bin" ;;
esac
dest: /etc/profile.d/hyperi-go.sh
owner: root
group: root
Expand Down
4 changes: 2 additions & 2 deletions ansible/roles/developer-rust/files/hyperi-rust-setup
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,8 @@ def install_sccache_linux(dry_run: bool, rep: Reporter) -> None:
"""Put the latest upstream sccache release at SCCACHE_BIN.

Re-runs are a no-op once the installed version matches the latest tag, so
the same call both installs and upgrades and hyperi-update needs no special
case.
the same call both installs and upgrades. Between converges hyperi-update
refreshes the same binary from the same release assets.
"""
target = _sccache_target()
if target is None:
Expand Down
703 changes: 554 additions & 149 deletions ansible/roles/developer/files/update/hyperi-update-linux.sh

Large diffs are not rendered by default.

96 changes: 61 additions & 35 deletions ansible/roles/developer/files/update/hyperi-update-macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@
# up on this machine, in one command.
#
# * Homebrew (formulae + casks: aws, gh, az, kubectl, helm,
# opentofu, openbao, gcloud-cli, ...)
# opentofu, openbao, gcloud-cli, claude-code, codex, ...)
# * macOS updates (softwareupdate) -- needs sudo
# * uv tools (gnome-extensions-cli, ...) -- user
# * rustup (Rust toolchains) -- user
# * uv Pythons (patch releases of each minor) -- user
# * rustup (Rust toolchains, where rustup is used) -- user
# * cargo tools (nextest, deny, cargo-audit, ...) -- user
# * go tools (gopls) -- user
# * npm globals (maid, semantic-release, pnpm) -- user
# * Claude Code CLI (self-installed under ~/.local) -- user
# * pnpm globals (eslint, prettier, typescript, ...) -- user
# * Codex plugin (claude plugin update) -- user
#
# Tier 3 static binaries (kind, argocd, kubeconform, ...) come from Homebrew
# formulae on macOS, so the Homebrew section already refreshes them -- the
# GitHub re-fetch is a Linux-only concern. The Codex CLI is the `codex` cask,
# so it rides that same section; only its Claude Code plugin, which brew knows
# nothing about, needs one of its own.
# The release binaries Linux fetches (kind, argocd, kubeconform, ...) come from
# Homebrew formulae on macOS, so the Homebrew section already refreshes them.
# Claude Code and the Codex CLI are casks and ride that same section. Only the
# Codex plugin for Claude Code, which brew knows nothing about, needs one of its
# own.
#
# Each section is independent and self-guarding: a tool that isn't installed is
# skipped (printed, not fatal), and a failing step is recorded and reported in
Expand All @@ -34,14 +35,17 @@
# hyperi-update --install (create the clickable "Hyperi Update" app)
# hyperi-update --help

set -u
set -o pipefail
# emulate resets every option to zsh's defaults, so the strict options come after
# it or a failing `softwareupdate | tee` would report success.
emulate -L zsh
setopt nounset pipefail

# Make user-level tools reachable even when launched from the GUI app or a
# non-login shell (Ansible): brew lives outside the base PATH on both Apple
# silicon and Intel.
export PATH="$HOME/.local/bin:${CARGO_HOME:-$HOME/.cargo}/bin:$HOME/go/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
# silicon and Intel, and brew's rustup is keg-only, so its shims are never linked
# into the brew prefix.
export PNPM_HOME="${PNPM_HOME:-$HOME/.local/share/pnpm}"
export PATH="$HOME/.local/bin:${CARGO_HOME:-$HOME/.cargo}/bin:$HOME/go/bin:$PNPM_HOME:$PNPM_HOME/bin:/opt/homebrew/bin:/opt/homebrew/opt/rustup/bin:/usr/local/bin:/usr/local/opt/rustup/bin:$PATH"

ASSUME_YES=0

Expand All @@ -50,8 +54,8 @@ SELF=${${(%):-%x}:A}

usage() {
cat <<EOF
hyperi-update — update Homebrew, macOS, uv tools, rustup, cargo/go/npm tools
and Claude Code in one go.
hyperi-update -- update Homebrew, macOS, uv tools, rustup and cargo/go/npm/pnpm
tools in one go.

Usage:
hyperi-update Confirm, then run all updates (prompts once for sudo).
Expand Down Expand Up @@ -159,12 +163,12 @@ if (( ! ASSUME_YES )); then
printf '%s%shyperi-update%s will update EVERYTHING on this Mac:\n\n' "$BOLD" "$BLUE" "$RESET"
have brew && printf ' - all Homebrew formulae and casks (including --greedy self-updaters)\n'
printf ' - macOS system and security updates\n'
have uv && printf ' - uv tools\n'
have uv && printf ' - uv tools and uv-managed Pythons\n'
have rustup && printf ' - rust toolchains\n'
have cargo-install-update && printf ' - cargo-installed tools\n'
have go && printf ' - go-installed tools (gopls)\n'
have npm && printf ' - npm global tools + pnpm\n'
have claude && printf ' - Claude Code CLI\n'
have pnpm && printf ' - pnpm global tools\n'
have claude && printf ' - the Codex plugin for Claude Code, if installed\n'
[[ -f "$ARCANE_DIR/compose.yaml" ]] && printf ' - Arcane (pull + recreate)\n'
printf '\nIt may take a while, and may ask to reboot at the end.\n\n'
Expand Down Expand Up @@ -236,8 +240,24 @@ else
skip "uv not found"
fi

# --- uv-managed Pythons ----------------------------------------------------
# Nothing else moves a uv-installed Python to a newer patch. `uv python upgrade`
# touches only the minors already installed and, without --default, adds no
# python or python3 shim. The superseded patch stays installed, since uv has no
# command that removes only those and a venv may still point at it.
section "uv Pythons"
if ! have uv; then
skip "uv not found"
elif [[ "$(uv python list --only-installed --managed-python --output-format json 2>/dev/null)" != *'"version"'* ]]; then
skip "uv manages no Pythons"
else
run "uv python upgrade" uv python upgrade
fi

# --- rustup toolchains -----------------------------------------------------
# rustup itself is updated by brew; this updates the toolchains it manages.
# Only a Mac that already had rustup keeps it. The role gives every other Mac
# brew's rust formula, which the Homebrew section updates. `rustup update` moves
# the toolchains, and rustup itself too unless brew owns it.
section "rustup toolchains"
if have rustup; then
run "rustup update" rustup update
Expand All @@ -258,24 +278,30 @@ else
fi

# --- go-installed tools ----------------------------------------------------
# No bulk updater for `go install` tools, so re-install @latest the ones that
# are already present (this adds nothing that was not there before).
# govulncheck is the brew formula on macOS, so Homebrew above refreshes it, and
# a `go install` here would put a second copy in ~/go/bin.
# No bulk updater for `go install` tools, so the one the role puts in ~/go/bin
# is re-installed @latest. govulncheck, gosec and flarectl are brew formulae on
# macOS, which the Homebrew section refreshes.
section "go tools"
GO_HOME="$HOME/go"
if have go; then
for gt in \
"gopls:golang.org/x/tools/gopls@latest"; do
bin="${gt%%:*}"; mod="${gt#*:}"
have "$bin" && run "go install $bin" go install "$mod"
done
# "<module> <version>" as built into the binary, so a gopls already at the
# module's latest release is not rebuilt.
built="$(go version -m "$GO_HOME/bin/gopls" 2>/dev/null | awk '$1 == "mod" {print $2, $3; exit}')"
if [[ ! -x "$GO_HOME/bin/gopls" ]]; then
skip "gopls not found in $GO_HOME/bin"
elif [[ -n "$built" ]] && [[ "$(go list -m -f '{{.Version}}' "${built% *}@latest" 2>/dev/null)" == "${built#* }" ]]; then
ok "gopls ${built#* } is current"
else
run "go install gopls" env GOPATH="$GO_HOME" GOBIN="$GO_HOME/bin" go install golang.org/x/tools/gopls@latest
fi
else
skip "go not found"
fi

# --- npm global tools ------------------------------------------------------
# maid, semantic-release, typescript, tsx, ts-node -- global npm packages; plus
# pnpm via corepack.
# --- npm and pnpm global tools ---------------------------------------------
# semantic-release and maid are npm globals. eslint, prettier, typescript, tsx
# and ts-node are pnpm globals, which `npm update -g` never sees. pnpm itself is
# corepack's, so it is re-activated at latest first.
section "npm global tools"
if have npm; then
run "npm update -g" npm update -g
Expand All @@ -284,13 +310,13 @@ else
skip "npm not found"
fi

# --- Claude Code -----------------------------------------------------------
# Run as the normal user (NOT under sudo) so it updates ~/.local, not root's.
section "Claude Code"
if have claude; then
run "claude update" claude update
section "pnpm global tools"
if have pnpm; then
# --latest, because the roles install each global at its latest release and
# the ranges pnpm recorded would otherwise hold them at that major.
run "pnpm update -g --latest" pnpm update -g --latest
else
skip "claude not found in PATH"
skip "pnpm not found"
fi

# --- Codex plugin for Claude Code ------------------------------------------
Expand Down
5 changes: 3 additions & 2 deletions ansible/roles/developer/tasks/removals.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@
become: "{{ ansible_facts['distribution'] != 'MacOSX' }}"
become_user: "{{ actual_user }}"

# Hand-installed copies of tools the roles install system-wide. ~/.local/bin and
# ~/go/bin precede /usr/local/bin and /usr/bin on PATH, so a copy left there
# Hand-installed copies of tools the roles install system-wide. ~/.local/bin
# precedes /usr/local/bin and /usr/bin on PATH, and so does ~/go/bin wherever a
# user's own profile prepends it (hyperi-go.sh appends it), so a copy left there
# shadows the managed install and no update ever reaches it. A deliberate pin
# of a managed tool there (an older kubectl, golangci-lint v1) goes too.
#
Expand Down
5 changes: 3 additions & 2 deletions ansible/roles/soe/tasks/colima.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
# - colima: runs docker-ce in a small Virtualization.framework VM -- the same
# engine Linux runs natively. Homebrew formula (Tier 1, brew upgrade sweeps it).
# - Apple `container`: native per-container micro-VMs on Apple silicon. Ships
# ONLY as a signed .pkg on GitHub releases (no brew channel), so it is a
# Tier 3 re-fetch -- hyperi-update pulls the latest. Optional: warn, never abort.
# ONLY as a signed .pkg on GitHub releases (no brew channel), so a re-run of
# this role installs the latest. hyperi-update does not touch it. Optional:
# warn, never abort.
# Both are wrapped warn-and-continue so a single failure does not sink the soe run.

- name: Install colima (Docker daemon for macOS)
Expand Down
11 changes: 6 additions & 5 deletions ansible/roles/soe/templates/hyperi-update.service.j2
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ Wants=network-online.target
[Service]
Type=oneshot
User={{ actual_user }}
# `--yes`: no prompts, and it never reboots under --yes. hyperi-update uses sudo
# for the system-package sweep, so unattended it needs NOPASSWD sudo (soe fleet
# users have it via add-dev-user); without that the sudo step fails and only the
# user-scoped language tools update. Tier-1 OS packages are also covered
# independently by unattended-upgrades / dnf-automatic (security.yml).
# `--yes`: no prompts, and it never reboots under --yes. hyperi-update needs sudo
# and has no terminal to ask for a password here, so unattended it needs
# NOPASSWD sudo (soe fleet users have it via add-dev-user). Without it the run
# exits 1 at the sudo check and updates nothing, the user-scoped tools included.
# Tier-1 OS packages are also covered independently by unattended-upgrades /
# dnf-automatic (security.yml).
ExecStart=/usr/local/bin/hyperi-update --yes
# A unit gets no PAM session, so a CARGO_HOME or RUSTUP_HOME declared in
# /etc/environment would otherwise never reach the cargo steps. `-`: the file
Expand Down
18 changes: 4 additions & 14 deletions docs/install-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ reach for instead, are not in that manifest and have no published digest at all.
| CLI utils (jq, gron, bat, fzf, ripgrep, fd, git-delta, moreutils, miller, rsync, tmux, htop, wget, shellcheck, age, parallel, ...) | all | distro repo / brew |
| sd | all | distro (apt/dnf) / brew |
| yq (mikefarah; apt `yq` is kislyuk/yq, a different tool) | all | Fedora dnf / Ubuntu re-fetch (Tier 3) / brew |
| lazygit | all | Fedora COPR / Ubuntu apt (re-fetch on 24.04 LTS) / brew |
| lazygit | all | Linux github-binary (Tier 3: re-fetch) / brew |
| docker (Engine on Linux, CLI-only on macOS) | all | vendor-repo / brew |
| git, git-lfs, git-filter-repo, gh | all | distro/PPA/brew |
| chrome, brave (opt-in `never`) | all | vendor-repo / cask |
Expand Down Expand Up @@ -628,19 +628,9 @@ security, so it holds the version it shipped with for the life of the release
(see the ladder note above). A vendor repo, a snap and Fedora's own packages
all track upstream properly.

**Tier 2 - language-manager tools.** Tools installed by uv / cargo / go / npm
have no OS channel; `hyperi-update` refreshes them via each manager
(`uv tool upgrade --all`, `rustup update && cargo install-update -a`,
`go install ...@latest`, `npm update -g`). E.g. ruff, ty, semgrep, pip-audit,
cargo-audit, cargo-hack, typos, govulncheck, maid.

**Tier 3 - static binaries.** A handful ship only as a GitHub-release binary with
no repo, snap, or language manager: kind, argocd, kubeconform, kube-linter,
aws-vault, dive, tea, terraform-docs, golangci-lint (plus a few tools on
whichever single distro lacks a package -- k9s, kustomize and yq are Ubuntu-only
here, since Fedora packages all three). `hyperi-update` re-fetches the latest
release for these, and skips the ones the running distro installs from a repo so
the binary cannot shadow the packaged copy.
**Tier 2 - language-manager tools.** Tools installed by uv / cargo / go / npm / pnpm have no OS channel, so `hyperi-update` refreshes them through each manager: `uv tool upgrade --all`, `rustup update` and `cargo install-update -a --locked`, `go install ...@latest` for the tools in `~/go/bin` (only when the module or the Go toolchain moved), `npm update -g`, and `pnpm update -g --latest`. The pnpm globals (eslint, prettier, typescript, tsx, ts-node) are installed unpinned, so `--latest` moves them to what a fresh converge would install, majors included. `uv python upgrade` moves each uv-managed Python to its newest patch without adding a python or python3 shim, and leaves the superseded patch installed, because uv has no command that removes only those. E.g. ruff, ty, semgrep, pip-audit, cargo-audit, cargo-hack, typos, govulncheck, maid.

**Tier 3 - static binaries.** A handful ship only as a release binary with no repo, snap, or language manager: kind, argocd, kubeconform, kube-linter, dive, terraform-docs, golangci-lint, lazygit, actionlint, osv-scanner, aws-vault, git-scrub, sccache, fnm, tea and macbash on both distros, k9s, kustomize, yq, hadolint, gitleaks and act on Ubuntu, and sd, kubectx and kubens on Fedora. `hyperi-update` re-fetches each one only where the role put it in `/usr/local/bin` on that distro, so the binary cannot shadow a packaged copy. Each download must match the sha256 GitHub publishes for the asset, or the release's checksum file where there is no digest (tea and macbash publish a `.sha256` beside the asset), and is renamed into place so the working copy is never half-written. aws-vault and Fedora's kubectx take the newest release at least 7 days old, as the role does. A stamp in `/var/lib/hyperi-update` stops an unmoved release being downloaded again, and the GitHub API is asked with the last ETag, so with a token set an unchanged release costs no rate limit (an anonymous 304 still counts). When the API refuses, the release document from the last run stands in. On Ubuntu, uv and uvx in `~/.local/bin` are refreshed the same way as the invoking user. gron, a Fedora release binary whose upstream has not released since 2022, is not refreshed.

golangci-lint is here for a different reason: Fedora does package it, but the
build trails upstream, and a linter behind the Go toolchain cannot read the
Expand Down
Loading