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
46 changes: 41 additions & 5 deletions .github/workflows/install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
run: su -s /bin/bash -c 'HOME=/home/testuser; export HOME; export TERM=xterm-256color; cd ~/dotfiles && bash test.sh ${{ matrix.profile }}' testuser

install-nosudo:
name: "nosudo @ Ubuntu ${{ matrix.ubuntu }}"
name: "nosudo-${{ matrix.variant }} @ Ubuntu ${{ matrix.ubuntu }}"
runs-on: ubuntu-latest
timeout-minutes: 40
container:
Expand All @@ -79,6 +79,9 @@ jobs:
fail-fast: false
matrix:
ubuntu: ["20.04", "22.04", "24.04"]
variant:
- auto # user has NO sudo binary; detect_sudo() auto-detects CAN_SUDO=false
- forced # user HAS passwordless sudo but NOSUDO=1 overrides it

steps:
- name: Bootstrap prerequisites (as root — mirrors a shared host)
Expand All @@ -87,7 +90,14 @@ jobs:
apt-get -yq update
apt-get -yq install apt-utils git curl wget ca-certificates zsh tmux python3

- name: Create non-root user WITHOUT sudo
- name: Install sudo and grant to user (nosudo-forced only)
if: matrix.variant == 'forced'
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get -yq install --no-install-recommends sudo
echo 'user ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers

- name: Create non-root user
run: useradd -m -s /bin/bash user

- name: Checkout dotfiles
Expand All @@ -105,10 +115,17 @@ jobs:
chown user:user /home/user/.curlrc
chmod 600 /home/user/.curlrc

- name: Install — no-sudo path (minimal profile)
- name: Install (${{ matrix.variant }})
env:
VARIANT: ${{ matrix.variant }}
run: |
if [ "$VARIANT" = "forced" ]; then
install_cmd="NOSUDO=1 bash install.sh minimal"
else
install_cmd="bash install.sh minimal"
fi
su -s /bin/bash -c \
'HOME=/home/user; export HOME; export PATH="$HOME/.local/bin:$PATH"; cd ~/dotfiles && bash install.sh minimal' \
"HOME=/home/user; export HOME; export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && $install_cmd" \
user

- name: Run test suite (nosudo profile)
Expand All @@ -118,7 +135,26 @@ jobs:
user

- name: Idempotency — re-run install.sh
env:
VARIANT: ${{ matrix.variant }}
run: |
if [ "$VARIANT" = "forced" ]; then
install_cmd="NOSUDO=1 bash install.sh minimal"
else
install_cmd="bash install.sh minimal"
fi
su -s /bin/bash -c \
"HOME=/home/user; export HOME; export PATH=\"\$HOME/.local/bin:\$PATH\"; cd ~/dotfiles && $install_cmd" \
user

- name: Run update.sh
run: |
su -s /bin/bash -c \
'HOME=/home/user; export HOME; export PATH="$HOME/.local/bin:$PATH"; cd ~/dotfiles && bash install.sh minimal' \
'HOME=/home/user; export HOME; export PATH="$HOME/.local/bin:$PATH"; cd ~/dotfiles && bash update.sh' \
user

- name: Re-run test suite after update
run: |
su -s /bin/bash -c \
'HOME=/home/user; export HOME; export PATH="$HOME/.local/bin:$PATH"; export TERM=xterm-256color; cd ~/dotfiles && bash test.sh nosudo' \
user
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ parallel-*/

# Local test run artifacts
.test-results/

# Neovim runtime artefacts (generated by lazy.nvim / neovim itself)
# nvim circular symlink: lazy.nvim adds the config dir to runtimepath via a self-referencing symlink
nvim/.config/nvim/nvim
# lazy-lock.json: plugin version lockfile — not tracked, always pull latest
nvim/.config/nvim/lazy-lock.json
103 changes: 101 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,104 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.4.0] - 2026-04-13

### Added
- `Dockerfile.nosudo`: parameterized test image covering two no-sudo install
scenarios, selected via build args:
- `nosudo-auto` (`GRANT_SUDO=false`, `NOSUDO_INSTALL=""`): no `sudo` binary
present; `detect_sudo()` auto-detects `CAN_SUDO=false` — mirrors bare Ubuntu
containers and shared hosts without sudo
- `nosudo-forced` (`GRANT_SUDO=true`, `NOSUDO_INSTALL=1`): user has passwordless
`sudo` but `NOSUDO=1` overrides it — tests that the explicit env-var override
is respected even when sudo would otherwise work
Both variants install the `minimal` profile and land all binaries in `~/.local/bin`.

### Changed
- `modules/tmux.sh`: removed `tmux-resurrect` and `tmux-continuum` from
`_TMUX_PLUGINS` — they were removed from `.tmux.conf.local` in v1.1.3 but the
installer was never updated; they were being cloned on every workstation install
without ever being sourced
- `test.sh`: comprehensive coverage improvements across all profiles:
- **Critical fix**: nosudo `sudo -v` check was `_fail` if sudo was present —
this always failed `nosudo-forced` (where user intentionally has sudo but
`NOSUDO=1` overrides it). Replaced with informational `_ok` for both variants;
the real invariant (binaries in `~/.local/bin`) is verified by `check_local_bin`
- `check_local_bin` made strict: no longer accepts system-path binaries via
`command -v` fallback — binary must be specifically at `~/.local/bin/<cmd>`
so a `NOSUDO=1` regression (binary landing in `/usr/local/bin`) is caught
- Added `delta`, `jq`, `python3` to core tools section (installed by every
profile; were only partially or never tested)
- Added `eza` and `shellcheck` to the minimal/workstation section
- Added eza and delta functional smoke tests to the nosudo section
- Added tmux plugin dir checks (`tmux-fzf`, `tmux-cpu`) for workstation profile
- Fixed `_skip()`: used `$*` where `$1` was intended, doubling the label text
- Fixed fd smoke test label ("can find files") to actually search files instead of
running `--version`
- Removed unreachable `else _fail "zoxide not installed"` (zoxide is now in
core tools, which already catches a missing binary)
- Added `nosudo` to the profile list in the usage comment
- `ci-local.sh` (renamed from `test-local.sh`): expanded nosudo coverage from one
scenario to two variants (`forced` / `auto`); added `--profile nosudo-forced` and
`--profile nosudo-auto` CLI selectors (`--profile nosudo` still selects both);
`_run_step` gained an optional `-u USER` flag so nosudo containers run tests as
the owning `user` rather than root; `run_nosudo` now runs the full 5-step pipeline
(install → test → idempotency → update → re-test) matching `run_combination` for
regular profiles; total default combinations increased from 12 to 15
- `.github/workflows/install.yml`: `install-nosudo` job expanded with
`variant: [auto, forced]` matrix axis (3 Ubuntu × 2 variants = 6 combinations,
up from 3); added `if: matrix.variant == 'forced'` step that installs `sudo`
and grants passwordless access only for the `forced` variant; added
`update.sh` step and re-test step so the no-sudo CI pipeline matches the
regular `install` job; total CI combinations increased from 12 to 15

### Fixed
- `get.sh` / `install.sh`: `NOSUDO=1 curl ... | bash` was silently ignored because
in a shell pipeline `VAR=val cmd1 | cmd2` the variable prefix is scoped only to
`cmd1` (curl), not `cmd2` (bash). Both scripts now accept `--nosudo` as a CLI flag
so the curl-pipe form works: `curl ... | bash -s -- --nosudo workstation`.
`NOSUDO=1 bash get.sh workstation` (local-file usage) is unchanged and still works.
- `README.md`: updated nosudo one-liner to use `--nosudo` flag instead of the broken
`NOSUDO=1 curl ...` form.
- `update.sh`: spurious `\"` in cheat URL pattern (`"linux-${cheat_arch}\""`) injected
a literal `"` into the grep pattern, making it impossible to match the `.gz` asset
URL. Cheat updates had been silently failing. Fixed to `"linux-${cheat_arch}.gz"`.
- `update.sh`: xcape update block used `local` and `trap ... RETURN` at script
top-level where `RETURN` traps never fire, leaking the mktemp directory on every
xcape rebuild. Extracted into `_do_update_xcape()` function so the trap fires
correctly on function return.
- `modules/base.sh`: no-sudo install paths for `delta`, `ripgrep`, `fd`, and `zoxide`
all constructed GitHub asset download URLs manually. CLAUDE.md forbids this because
asset names change between releases (delta 0.19.0 renamed its tarball). All four now
use `_gh_release_info` to look up the actual asset URL from the API, matching the
pattern already used by the sudo paths.
- `modules/zsh.sh` `_install_ohmyzsh`: `sh -c "$($installer)"` — if curl/wget failed,
`sh -c ""` returned 0 and `set -e` never triggered, silently reporting success while
oh-my-zsh was never installed. Now downloads the script into a variable with an
explicit failure guard before passing to `sh -c`. Installer failure also now warns
and returns instead of triggering `set -e`.
- `update.sh` oh-my-zsh update: `zsh ... || git pull` — if `git pull` (the fallback)
failed, `set -e` exited the entire update script, leaving all remaining tools not
updated with no warning message. Rewritten as `if/elif/else` that logs a warning
and continues.
- `lib/utils.sh` `_download_tar_bin`: no `trap RETURN` and no `|| return 1` on the
pipe, so a download failure triggered `set -e` exit inside the function — callers'
`|| log_warn` handlers were never reached. Added `trap RETURN` for cleanup and
`|| return 1` so failures propagate correctly to callers.
- `modules/neovim.sh` `install_neovim`, `update.sh` `_do_update_neovim`,
`update.sh` `_do_update_uv`, `update.sh` cheat update,
`modules/base.sh` `_install_eza` PPA: all had `curl | tar/gpg/gunzip` pipes with
no error handlers. A network failure would exit the whole script mid-run with no
user message. Each now logs a warning and returns/skips cleanly.
- `scripts/install-git.sh` / `scripts/install-cmake.sh`: `curl | tar` and `curl -o`
download failures were silently caught by `set -euo pipefail`, exiting the script
with no message. Both now use `|| die "…"` so network failures print an actionable
error before exiting.
- `modules/base.sh` `_install_eza` (no-sudo path) and `_install_jq`: used
`_gh_latest_tag_noapi` followed by manual URL construction — the same fragile
pattern that CLAUDE.md prohibits because asset names change between releases.
Both now use `_gh_release_info` to look up the verified asset URL in one API call.

## [1.3.0] - 2026-04-11

### Added
Expand Down Expand Up @@ -232,7 +330,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`zsh` is not available; prints actionable reinstall hint
- `test.sh`: `nosudo` profile — verifies all `~/.local/bin` binaries are present
and functional; confirms `sudo` is absent in the test environment
- `test-local.sh`: `--profile nosudo` flag and `--skip-nosudo` flag; `run_nosudo`
- `ci-local.sh`: `--profile nosudo` flag and `--skip-nosudo` flag; `run_nosudo`
runner builds via `Dockerfile.nosudo` and runs the nosudo test suite
- `.github/workflows/install.yml`: `install-nosudo` job — matrix across Ubuntu
20.04 / 22.04 / 24.04 as a non-root user with no `sudo`
Expand Down Expand Up @@ -332,7 +430,8 @@ Complete overhaul of the dotfiles infrastructure: modular profiles, Neovim, CI,
### Added
- Initial dotfiles: Zsh (oh-my-zsh + fzf), Tmux, Vim, and monolithic `install.sh`

[Unreleased]: https://github.com/YASoftwareDev/dotfiles/compare/v1.3.0...HEAD
[Unreleased]: https://github.com/YASoftwareDev/dotfiles/compare/v1.4.0...HEAD
[1.4.0]: https://github.com/YASoftwareDev/dotfiles/compare/v1.3.0...v1.4.0
[1.3.0]: https://github.com/YASoftwareDev/dotfiles/compare/v1.2.5...v1.3.0
[1.2.5]: https://github.com/YASoftwareDev/dotfiles/compare/v1.2.4...v1.2.5
[1.2.4]: https://github.com/YASoftwareDev/dotfiles/compare/v1.2.3...v1.2.4
Expand Down
33 changes: 27 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ CI tests all profile × Ubuntu-version combinations on every push.
install.sh entry point — profile selection, module orchestration
update.sh managed tool updates with --check mode + PATH shadow check
test.sh post-install validation suite (profile-aware)
ci-local.sh local Docker matrix runner — mirrors GitHub CI matrix
get.sh curl-pipe bootstrap (clones repo → runs install.sh)
Dockerfile bakes docker profile into an image at build time
Dockerfile.nosudo parameterized no-sudo test image (two variants: forced/auto)
lib/utils.sh shared logging, sudo detection, GitHub helpers, binary utils
modules/
base.sh apt packages + per-tool installers (fzf, zoxide, delta, eza, …)
Expand Down Expand Up @@ -316,10 +319,24 @@ bash test.sh [docker|minimal|workstation|nosudo]
Exits 0 (all pass), 1 (any fail). Skips are not failures.
Runs in CI after every install + again after update.sh.

Core checks (all profiles): zsh, tmux, git, python3, fzf, ripgrep, git-delta,
zoxide, jq, fd; fzf shell integration + functional filter; zsh syntax check;
oh-my-zsh + plugins + powerlevel10k dirs; tmux detached session start; git config
diff driver; zoxide init + add + query.

Profile-specific checks:
- `workstation` — nvim, delta, uv, cheat, config symlinks
- `minimal` — ranger, tig, parallel
- `nosudo` — all `~/.local/bin` binaries present + sudo NOT available
- `workstation` — nvim, uv, cheat; config symlinks (nvim, ripgrep, ranger);
tmux plugin dirs (tmux-fzf, tmux-cpu)
- `minimal` — ranger, tig, parallel, eza, shellcheck (all via apt)
- `nosudo` — strict `~/.local/bin` presence check for all 7 GitHub binaries
(rg, fd, jq, fzf, zoxide, delta, eza); sudo availability info (not a failure
condition — nosudo-forced has sudo available); functional smoke tests for each

Two nosudo scenarios are validated by `Dockerfile.nosudo`:
- **nosudo-auto** — no `sudo` binary; `detect_sudo()` auto-detects `CAN_SUDO=false`
(`GRANT_SUDO=false NOSUDO_INSTALL=""` build args)
- **nosudo-forced** — user has passwordless `sudo` but `NOSUDO=1` overrides it
(`GRANT_SUDO=true NOSUDO_INSTALL=1` build args)

---

Expand All @@ -330,10 +347,14 @@ Profile-specific checks:
- testuser with passwordless sudo; curl auth via `~/.curlrc`
- Flow: install → idempotency re-run → test → update → re-test

**Job `install-nosudo`** (3 combinations):
- Ubuntu 20.04 / 22.04 / 24.04
**Job `install-nosudo`** (6 combinations — 3 Ubuntu × 2 variants):
- Ubuntu 20.04 / 22.04 / 24.04 × variant `auto` / `forced`
- Root pre-installs: git, curl, wget, ca-certificates, zsh, tmux, python3
- Regular user with no sudo; `NOSUDO=1` path exercised
- `auto`: no `sudo` binary installed; `detect_sudo()` auto-detects `CAN_SUDO=false`
- `forced`: passwordless `sudo` installed, but `NOSUDO=1` overrides it
- Flow: install → test → idempotency re-run → update → re-test

**Total: 15 CI combinations** (9 regular + 6 nosudo)

---

Expand Down
82 changes: 82 additions & 0 deletions Dockerfile.nosudo
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Dockerfile.nosudo
#
# Covers two user-local install scenarios (both land all binaries in ~/.local/bin):
#
# nosudo-auto — user has NO sudo; detect_sudo() auto-detects CAN_SUDO=false.
# Ubuntu base images ship without sudo, so no extra setup needed.
# Build args: GRANT_SUDO=false (default), NOSUDO_INSTALL="" (default)
#
# nosudo-forced — user HAS passwordless sudo but NOSUDO=1 overrides it.
# Tests that the explicit override is respected even when sudo works.
# Build args: GRANT_SUDO=true, NOSUDO_INSTALL=1
#
# Used automatically by ci-local.sh (runs both variants) or directly:
#
# # nosudo-auto (default)
# docker build --build-arg UBUNTU=24.04 \
# -t dotfiles-test:24.04-nosudo-auto -f Dockerfile.nosudo .
#
# # nosudo-forced
# docker build --build-arg UBUNTU=24.04 \
# --build-arg GRANT_SUDO=true --build-arg NOSUDO_INSTALL=1 \
# -t dotfiles-test:24.04-nosudo-forced -f Dockerfile.nosudo .
#
# Then run the test suite (ci-local.sh does this automatically):
# docker run --rm --user user -e TERM=xterm-256color \
# -e POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true \
# <tag> bash -c \
# 'export PATH="$HOME/.local/bin:$PATH"; cd ~/dotfiles && bash test.sh nosudo'

ARG UBUNTU=24.04
FROM ubuntu:${UBUNTU}

ENV DEBIAN_FRONTEND=noninteractive
ENV TERM=xterm-256color
ENV POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD=true

# ARGs re-declared after FROM so they are available in RUN instructions.
# GRANT_SUDO=false : don't install sudo package (nosudo-auto)
# sudo binary absent → detect_sudo() → CAN_SUDO=false
# GRANT_SUDO=true : install sudo + add user to sudoers (nosudo-forced)
# sudo works, but NOSUDO=1 overrides below
ARG GRANT_SUDO=false

# NOSUDO_INSTALL="" : no override — detect_sudo() decides (nosudo-auto)
# NOSUDO_INSTALL=1 : NOSUDO=1 passed to installer — forces user-local (nosudo-forced)
ARG NOSUDO_INSTALL=

# ── Step 1-2: Base prerequisites (always installed) ───────────────────────────
RUN apt-get -yq update && \
apt-get -yq install --no-install-recommends \
apt-utils git curl wget ca-certificates zsh tmux python3 && \
rm -rf /var/lib/apt/lists/*

# ── Step 2b: Optionally install sudo (nosudo-forced only) ─────────────────────
# nosudo-auto intentionally omits the sudo binary so detect_sudo() auto-detects.
# nosudo-forced needs a working sudo so the NOSUDO=1 override is meaningful.
RUN if [ "${GRANT_SUDO}" = "true" ]; then \
apt-get -yq update && \
apt-get -yq install --no-install-recommends sudo && \
rm -rf /var/lib/apt/lists/*; \
fi

# ── Step 3: Create non-root user; conditionally grant passwordless sudo ───────
RUN useradd -m -s /bin/bash user && \
if [ "${GRANT_SUDO}" = "true" ]; then \
echo 'user ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers; \
fi

# ── Step 4: Copy dotfiles source owned by the non-root user ──────────────────
COPY --chown=user:user . /home/user/dotfiles

# ── Step 5: Switch to non-privileged user ─────────────────────────────────────
USER user
WORKDIR /home/user

# ── Step 6: Install ────────────────────────────────────────────────────────────
# NOSUDO="${NOSUDO_INSTALL}" is "" for nosudo-auto (detect_sudo decides)
# and "1" for nosudo-forced (overrides working sudo).
# 'minimal' profile: zsh, tmux, git config + ~7 GitHub-tarball binaries.
RUN cd dotfiles && NOSUDO="${NOSUDO_INSTALL}" bash install.sh minimal

CMD ["bash"]
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ curl -fsSL https://raw.githubusercontent.com/YASoftwareDev/dotfiles/master/get.s
**nosudo** — any profile without apt/sudo; all tools land in `~/.local/bin` (~10–20 min)

```bash
NOSUDO=1 curl -fsSL https://raw.githubusercontent.com/YASoftwareDev/dotfiles/master/get.sh | bash -s -- workstation
curl -fsSL https://raw.githubusercontent.com/YASoftwareDev/dotfiles/master/get.sh | bash -s -- --nosudo workstation
# or, after cloning:
NOSUDO=1 ./install.sh workstation
```
Expand Down Expand Up @@ -237,8 +237,10 @@ cd ~/.dotfiles && ./update.sh
~/.dotfiles/
├── install.sh # main entry point — profile wizard + runner
├── update.sh # updater for all managed tools and plugins
├── test.sh # integration tests
├── Dockerfile # for testing the docker profile
├── test.sh # integration tests (profile-aware)
├── ci-local.sh # local Docker matrix runner — mirrors GitHub CI
├── Dockerfile # for baking dotfiles into a Docker image (docker profile)
├── Dockerfile.nosudo # parameterized test image for no-sudo install scenarios
├── lib/
│ └── utils.sh # shared: logging, symlink, apt_install, GitHub helpers, checks
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.3.0
1.4.0
Loading
Loading