- Integration tests now run the full bootstrap (system + flatpak + custom +
AI + VM packages) instead of '--only custom --no-vm --no-ai', so that
custom-package prerequisites (zsh, gh, etc.) installed via SystemPackages
are actually available.
- Fix 'pnpm setup' failing with ERR_PNPM_UNKNOWN_SHELL in CI containers
by exporting SHELL=/bin/bash before invoking it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add env: GITHUB_TOKEN to the Linux workflow step, then read it in
ci/main.go and inject it into each test container via WithSecretVariable
(for both GITHUB_TOKEN and GH_TOKEN). This prevents 403 rate-limit
errors on GitHub API calls (neovim/nvm releases) and authenticates
gh CLI for gh extension install inside the containers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Unauthenticated GitHub API calls share the runner IP (60 req/hour limit).
net.go already uses GITHUB_TOKEN as a Bearer token when present.
gh CLI also needs GH_TOKEN to authenticate for gh extension install.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Track error count in issues.go alongside the existing issues slice.
Add hasErrors() helper. In main.go, call osExit(1) after writeRunLog()
if any [ERROR] entries were recorded, so CI steps correctly fail when
errors occur (e.g. GitHub API 403 rate-limit hits in the macOS job).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add explicit token inputs to the checkout and softprops/action-gh-release
steps so the GitHub token is wired through instead of relying on implicit
defaults. The workflow-level contents: write permission already scopes
the token correctly for tag/release creation.
Pushing a tag matching v* (or running the workflow manually with an
existing tag) cross-compiles the four supported targets via
`make build-all`, generates a SHA256SUMS file, and publishes a GitHub
release with all binaries attached.
Replace the Python bootstrap script with a Go implementation that
cross-compiles to native binaries for Linux and macOS on x86_64 and
aarch64. The Go port preserves all sections of the original (system
packages, optional Flatpak GUI apps, custom downloads, and the macOS
firecracker VM bridge) and adds first-class pacman support so the tool
works on Arch-family distros alongside Debian, RHEL, and macOS.
A Makefile produces a host binary via `make build` and the full
four-target matrix under dist/ via `make build-all`.
When python3-full is not installed on Debian/Ubuntu (common with Python
3.13), the _decimal C extension fails to import, crashing any pip
invocation with RuntimeError. Add _python3_decimal_ok() to probe for
this, and _fix_python3_decimal() to install python3-full (apt) or
python3-libs (dnf). _install_pip() now calls these before attempting
any ensurepip/pip operations so the error is fixed automatically rather
than producing an unrecoverable crash.
https://claude.ai/code/session_01Lrg3UV7AJN9KRXyTTWGgZi
`run()` and `shell()` now default to a 30-minute cap (generous enough for
heavy apt/brew installs and large downloads, bounded enough to catch a
true hang). Callers can override per-call.
All remaining direct subprocess.run sites have explicit timeouts sized to
the work they do: short caps for read-only probes (rpm, dpkg, brew list,
flatpak info, gh auth status, xcode-select -p, sysctl, VBoxManage), a
60-minute cap for the pyenv Python compile, a 15-minute cap for the
interactive `gh auth login` browser flow, and ConnectTimeout+30s for the
VM SSH helper. A small `_probe()` wrapper centralizes the probe pattern.
On timeout the bootstrap now warns and either fails the check gracefully
or returns rc=124, rather than blocking indefinitely.
https://claude.ai/code/session_01447pnRM4ogyxs5tVCUNDZW
A hung `python3 -m pip --version` inside `_pip_installed()` could block
the bootstrap indefinitely, since `subprocess.run` was called without a
timeout. Treat a timeout (or a missing python3) as "pip not installed"
so the bootstrap proceeds to install it rather than hanging.
https://claude.ai/code/session_01447pnRM4ogyxs5tVCUNDZW
On re-runs, ~/.config/nvim is moved aside to ~/.config/nvim-1
(or nvim-2, nvim-3, ... — first free number) and a fresh clone is
created. End-of-run notices report the backup path so it's visible
after the rest of the bootstrap output scrolls by.
Previously rmtree'd ~/.config/nvim on every full bootstrap run, destroying
any user edits, plugin state, and undo history. Now skips when the
directory is already a clone of the expected repo, and refuses to touch
unfamiliar contents (different remote, or non-git directory).
The PATH line written to /etc/profile.d/neovim.sh was only sourced by
login shells via /etc/profile. zsh — set as the default shell by this
script — uses /etc/zsh/zprofile which sources /etc/profile only for
login shells, and terminal emulators typically launch non-login
interactive shells. The result on Debian 13 ARM64 (and other distros):
nvim installed correctly to /opt/nvim-linux-arm64 but was not on PATH
in a normal terminal session.
Symlink the binary into /usr/local/bin/nvim instead — that directory
is always on the default PATH on Linux and macOS (Apple Silicon and
Intel) regardless of shell type. The Zig install already follows this
pattern. Also switch the install-detection probe to the symlink so it
verifies an actually-callable nvim, not just the extracted directory.
https://claude.ai/code/session_01VP9hCLH1c5F5iv468jSkqs
Pulumi does not publish apt or yum repositories — the URLs the setup
function referenced (api.pulumi.com/releases/sdk/{apt,rpm}-keyring.gpg
and {apt,yum}.releases.pulumi.com) return 404 / do not resolve, which
broke `bootstrap_environment.py` on every fresh Linux run.
Replace `setup_pulumi_repo` with a special-package installer that
downloads the official GitHub release tarball, verifies the SHA256
against the published checksums file, extracts to /opt/pulumi, and
exposes the binaries via PATH — the same pattern used for Neovim.
macOS continues to install Pulumi via brew.
https://claude.ai/code/session_018L1gFoc8L3CYqLE2wNjpy6
- dotnet-sdk-10.0: add setup_dotnet_repo() to register the Microsoft apt
feed (packages.microsoft.com) before attempting the install
- lua: map to lua5.4 in apt-get overrides (Debian has no unversioned lua pkg)
- pulumi: add setup_pulumi_repo() to register apt.releases.pulumi.com before
attempting the install; add dnf/yum variant too
- qemu: map to qemu-system in apt-get overrides (Debian meta-package name)
- python3 -m ensurepip: Debian intentionally strips ensurepip from the system
Python package; fall back to apt-get install python3-pip automatically
Also add libnsl2 → libnsl-dev mapping for Debian (bonus pyenv build fix).
https://claude.ai/code/session_017KuwqSq7nCp2iWiTeaNivn
- Inverts the GUI opt-out model: GUI packages and Flatpak are now skipped
by default (headless-friendly), and --gui opts in to installing them.
Removes --no-gui entirely.
- Adds lazygit and pulumi to the default system package list.
https://claude.ai/code/session_01CsTAu2SNhE5ZAQm3RnVwp3
Three root causes were causing failures on non-x86_64-Linux platforms:
1. Single SHA256 per package: the pinned fallback checksum in
formatted_packages.py was a single value computed for linux-x86_64 only.
Downloads on linux-aarch64, macos-x86_64, and macos-aarch64 produced
different binaries with different digests, so verification always failed
on those platforms when the fetch_latest resolver was unavailable.
Fix: replace the single `sha256` field with a `sha256_map` dict keyed by
"{os}-{arch}" (e.g. "linux-aarch64", "macos-arm64"). Added the correct
pinned digests for all four supported platforms for both Go 1.26.3 and
Firecracker 1.15.1, fetched from official sources.
2. Case-sensitive SHA256 comparison: _sha256_of() always returns lowercase
hex, but checksums returned by external APIs could be uppercase.
_verify() compared them without normalising case, causing false mismatches.
Fix: new resolved_sha256 property always returns a lowercased digest;
_resolve_latest() also lowercases the dynamically-fetched sha before
storing it.
3. Firecracker not skipped on macOS: _resolve_latest_firecracker() returns
None on macOS (firecracker is Linux-only), causing a fallback to the
pinned linux binary URL with a linux-only checksum map entry. The download
and verification would both fail misleadingly.
Fix: explicit early-continue guard in install_custom_packages() when
name == "firecracker" and IS_MACOS.
Adds curl, ncurses-devel, xz (tool), libxml2-devel, and xmlsec1-devel to
SYSTEM_PACKAGES to cover the full pyenv/CPython build dependency set.
Also adds apt-get overrides for all Fedora-named Python build packages
(bzip2-devel → libbz2-dev, openssl-devel → libssl-dev, etc.) so they
resolve to the correct Debian/Ubuntu package names at install time.
Adds corresponding brew overrides for the new packages.
https://claude.ai/code/session_01P3CkL5oXzkTvayityg3uaH
When the system has disk I/O errors (errno 5), subprocess.run raises
OSError before a process can even start. The run() and shell() helpers
now catch OSError: if check=False the error is logged as a warning and
a returncode=1 CompletedProcess is returned so callers like
install_system_packages can continue; if check=True the error is
re-raised as before.
https://claude.ai/code/session_01Gsfw2QLhQiFVEvZQC5nVcU
- curl and unzip were pipe-chained, but -o writes to a file so the pipe
produced nothing; split into separate steps with &&
- Extracted directory is bootstrap_dev_env-main, not bootstrap
- Script is bootstrap_environment.py, not bootstrap_dev_env
https://claude.ai/code/session_01CkJR1eHoBVMyNNeGdbRBYK
Two issues caused failures on Debian 13 (Trixie) ARM64:
1. apt-get update was never called before installing gnupg, leaving the
package cache stale. The cached version (gnupg2 2.4.7-21+b3) was no
longer available on the ARM64 mirror, producing 404 errors. Adding
apt-get update before the install fixes this.
2. The Docker GPG key and apt repo URL were hardcoded to the Ubuntu
endpoint. Debian has its own Docker repo at
https://download.docker.com/linux/debian. The distro ID is now read
from /etc/os-release and used to select the correct URL.
https://claude.ai/code/session_01CkJR1eHoBVMyNNeGdbRBYK
Pick a hypervisor based on the host's actual nested-virt capability
instead of always using QEMU/HVF (which doesn't expose nested KVM):
* Apple Silicon M3+ on macOS 15 Sequoia+: QEMU/HVF with
-cpu host,el2=on so the Linux guest's KVM (and therefore
firecracker microVMs) actually works.
* Intel Mac: VirtualBox with --nested-hw-virt on. Cask is
installed on demand; the seed ISO and SSH key flow are shared
with the QEMU path.
* Apple Silicon M1/M2 (any macOS), or M3+ pre-Sequoia: skip the
whole VM step and print a notice pointing at a Linux cloud VM
as the only path to firecracker on that hardware.
Backend selection is centralized in _select_vm_backend(); the rest
of setup_firecracker_vm (image download/verify, cloud-init seed,
SSH wait, in-VM firecracker probe, zsh wrapper) is shared.
https://claude.ai/code/session_01R2CnCeEBYT5QoHiwxczDNq
After the normal package install on macOS, fetch the latest Fedora
cloud qcow2 (auto-discovered from dl.fedoraproject.org), verify its
SHA256, build a cloud-init seed ISO with hdiutil, boot it under
QEMU/HVF (UEFI on aarch64, q35 on x86_64), and wait for cloud-init
to install firecracker inside the guest.
Then write a `firecracker()` function block into ~/.zshrc that
starts the backing VM on demand and proxies invocations via SSH.
The host's own firecracker custom-package install is dropped on
macOS since it's Linux-only. Suppress the whole flow with --no-vm.
https://claude.ai/code/session_01R2CnCeEBYT5QoHiwxczDNq
Detect Darwin automatically and, on macOS, install the Xcode Command
Line Tools and Homebrew as the first actions before any package work.
Route system packages through brew (formulae and casks), skip the
Flatpak section entirely, refuse to run as root (brew won't), and
adjust custom-package URL/install-path templates with new {os}, {os_go},
{os_zig}, {os_nvim} substitutions. Architecture detection remains
dynamic on both Apple Silicon and Intel.
https://claude.ai/code/session_01R2CnCeEBYT5QoHiwxczDNq
- Add zsh to SYSTEM_PACKAGES
- Detect RHEL-family vs Debian-family from /etc/os-release ID/ID_LIKE
(falls back to PKG_MGR if os-release is unreadable)
- ensure_zsh_default sets zsh as the invoking user's login shell after
system install: usermod -s on RHEL-family (chsh under default
authselect refuses other-user changes), chsh -s elsewhere; SUDO_USER
is preferred so sudo invocations target the real user
- Add oh-my-zsh to CUSTOM_PACKAGES; the installer runs the official
unattended script (requires zsh + git, which are already installed by
this point) and rewrites ZSH_THEME to "gnzh" in ~/.zshrc
- As a final step, run 'zsh -c "source ~/.zshrc"' to validate the rc
file (the user's interactive shell is unaffected — they need a new
terminal to pick up the new default shell)