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
96 changes: 96 additions & 0 deletions .github/workflows/install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,99 @@ jobs:
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

install-nosudo-alma:
name: "nosudo-${{ matrix.variant }} @ AlmaLinux ${{ matrix.alma }}"
runs-on: ubuntu-latest
timeout-minutes: 40
container:
image: "almalinux:${{ matrix.alma }}"

strategy:
fail-fast: false
matrix:
alma: ["9", "10"]
variant:
- auto # user has NO sudo binary; detect_sudo() auto-detects CAN_SUDO=false
- forced # user HAS passwordless sudo but NOSUDO=1 overrides it
- nonsudoer # sudo binary present but user NOT in sudoers; auto-detects CAN_SUDO=false

steps:
- name: Bootstrap prerequisites (as root - mirrors a shared RHEL host)
# --allowerasing: EL9+ images ship curl-minimal, which conflicts with
# the full curl package - allow dnf to swap it out.
run: |
dnf -yq install --allowerasing git curl wget ca-certificates zsh tmux python3 \
tar gzip xz findutils shadow-utils glibc-langpack-en

- name: Install sudo binary (forced + nonsudoer)
if: matrix.variant == 'forced' || matrix.variant == 'nonsudoer'
run: dnf -yq install sudo

- name: Grant passwordless sudo to user (forced only)
# nonsudoer deliberately leaves the user OUT of sudoers so detect_sudo()
# must auto-detect CAN_SUDO=false against a present-but-unauthorised sudo.
if: matrix.variant == 'forced'
run: echo 'user ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers

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

- name: Checkout dotfiles
uses: actions/checkout@v4

- name: Copy dotfiles to user home
run: |
cp -r . /home/user/dotfiles
chown -R user:user /home/user/dotfiles

- name: Configure curl auth (avoids GitHub API rate limit)
run: |
printf 'header = "Authorization: Bearer %s"\n' "${{ secrets.GITHUB_TOKEN }}" \
> /home/user/.curlrc
chown user:user /home/user/.curlrc
chmod 600 /home/user/.curlrc

- 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 && $install_cmd" \
user

- name: Run test suite (nosudo profile)
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

- 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 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
14 changes: 10 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ This file provides codebase context for AI coding agents (Codex, Copilot, etc.).

## Repository purpose

Personal dotfiles for Ubuntu/Debian: one-command install (`install.sh`), managed
updates (`update.sh`), post-install test suite (`test.sh`), and CI matrix covering
3 Ubuntu versions × 3 install profiles + 3 no-sudo variants (auto / forced /
nonsudoer) - 18 cells total.
Personal dotfiles for Ubuntu/Debian, with RHEL-family (AlmaLinux/Rocky/Fedora)
support via the user-local binary path: one-command install (`install.sh`),
managed updates (`update.sh`), post-install test suite (`test.sh`), and CI
matrix covering 3 Ubuntu versions × 3 install profiles + no-sudo variants
(auto / forced / nonsudoer) on 3 Ubuntu + 2 AlmaLinux versions - 24 cells total.

## Key files

Expand All @@ -23,6 +24,11 @@ nonsudoer) - 18 cells total.

## Critical rules (never violate)

- Gate every apt code path on `$CAN_APT` (sudo AND apt-get present, set by
`detect_sudo`), never on `$CAN_SUDO` alone - on RHEL-family systems sudo can be
available while apt is not, and a bare `apt-get` call dies under `set -e`.
Applies to the install/update flow; the optional `scripts/` desktop helpers
are Ubuntu-only.
- All scripts use `set -euo pipefail`. Use `count=$(( count + 1 ))` - never `(( count++ ))`.
Also avoid `[ cond ] && var=true` - when the condition is false, the expression exits with 1 and
trips `set -e`. Use `if [ cond ]; then var=true; fi` instead.
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- RHEL-family support (AlmaLinux, Rocky, CentOS Stream, Fedora), including
no-sudo accounts: a new `CAN_APT` global (sudo AND apt-get present, set by
`detect_sudo`) gates every apt code path in the install/update flow
(`install.sh`, `update.sh`, `modules/`; the optional `scripts/` desktop
helpers remain Ubuntu-only), so non-apt systems take the
user-local binary path (`~/.local/bin`) with or without sudo instead of
dying on a missing `apt-get` under `set -e`. This includes neovim: on
non-apt systems `install.sh` and `update.sh` place it under `~/.local`
even when sudo is available (`/usr/local` remains the destination on apt
systems with sudo). Prerequisite hints are now
package-manager-aware (`_pkg_install_hint`: apt/dnf/yum), `get.sh` bootstraps
git/curl via dnf/yum when run as root on RHEL, and `_check_os`/`get.sh`
recognise RHEL-family IDs instead of warning "not Ubuntu/Debian".
- CI coverage for the RHEL path: `Dockerfile.nosudo` takes a `BASE` image arg
(Ubuntu or AlmaLinux, apt/dnf branch, `--allowerasing` for the EL9+
curl-minimal conflict), `ci-local.sh` gains `--alma` and AlmaLinux 9/10
no-sudo cells, and a new `install-nosudo-alma` GitHub Actions job runs all
three variants on AlmaLinux 9 and 10 (18 -> 24 cells).
- `ci-local.sh`: local runs now pass a GitHub token (from `GH_TOKEN` or
`gh auth token`) into builds via BuildKit secret and into runs via env, so
API-based installers no longer trip the unauthenticated 60-req/hr rate limit
(the documented false-failure mode of local no-sudo cells).
- `zsh/.zshrc`: prepend `$HOME/go/bin` to `PATH` so `go install`-ed binaries are
found (no-op when the directory doesn't exist).

Expand Down
12 changes: 10 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Test dotfiles install in a clean Ubuntu environment.
# Requires BuildKit (Docker 20.10+ default) for the optional gh_token secret.
#
# Build & run (installs during build, then drops to shell):
# docker build -t dotfiles-test:24.04-docker .
Expand Down Expand Up @@ -29,7 +30,14 @@ RUN apt-get -yq update && \
WORKDIR /root/dotfiles
COPY . .

RUN bash install.sh ${PROFILE} && \
chsh -s /usr/bin/zsh root
# Optional gh_token secret -> ~/.curlrc auth header for the install only
# (avoids GitHub's unauthenticated API rate limit); removed after install.
RUN --mount=type=secret,id=gh_token,mode=0444,required=false \
tok="$(cat /run/secrets/gh_token 2>/dev/null || true)"; \
if [ -n "$tok" ]; then \
printf 'header = "Authorization: Bearer %s"\n' "$tok" > ~/.curlrc; \
fi; \
bash install.sh ${PROFILE} && chsh -s /usr/bin/zsh root; \
rc=$?; rm -f ~/.curlrc; exit $rc

CMD ["zsh"]
70 changes: 54 additions & 16 deletions Dockerfile.nosudo
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Dockerfile.nosudo
# Requires BuildKit (Docker 20.10+ default) for the optional gh_token secret.
#
# Covers three user-local install scenarios (all land 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.
# Ubuntu/AlmaLinux 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.
Expand All @@ -17,25 +19,32 @@
# misclassified as sudo_password.
# Build args: INSTALL_SUDO=true, GRANT_SUDO=false, NOSUDO_INSTALL=""
#
# The BASE arg selects the distro (apt- and dnf-based images both work):
# ubuntu:24.04 (default), ubuntu:22.04, ubuntu:20.04, almalinux:9, almalinux:10
#
# Used automatically by ci-local.sh (runs all variants) or directly:
#
# # nosudo-auto (default)
# docker build --build-arg UBUNTU=24.04 \
# -t dotfiles-test:24.04-nosudo-auto -f Dockerfile.nosudo .
# # nosudo-auto on AlmaLinux 9
# docker build --build-arg BASE=almalinux:9 \
# -t dotfiles-test:alma9-nosudo-auto -f Dockerfile.nosudo .
#
# # nosudo-forced
# docker build --build-arg UBUNTU=24.04 \
# # nosudo-forced on Ubuntu 24.04
# docker build --build-arg BASE=ubuntu:24.04 \
# --build-arg GRANT_SUDO=true --build-arg NOSUDO_INSTALL=1 \
# -t dotfiles-test:24.04-nosudo-forced -f Dockerfile.nosudo .
#
# Optional: pass a GitHub token so the ~6 API-based installers don't hit the
# unauthenticated 60-req/hr rate limit (the known local-run failure mode):
# docker build --secret id=gh_token,src=/path/to/tokenfile ...
#
# 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}
ARG BASE=ubuntu:24.04
FROM ${BASE}

ENV DEBIAN_FRONTEND=noninteractive
ENV TERM=xterm-256color
Expand All @@ -59,20 +68,37 @@ ARG INSTALL_SUDO=false
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/*
# Branch on the image's package manager: apt (Ubuntu/Debian) or dnf (AlmaLinux/
# RHEL family). The dnf list adds what Ubuntu images already carry: tar/gzip/xz
# (binary extraction), findutils (_download_tar_bin uses find), shadow-utils
# (useradd below), glibc-langpack-en (en_US.UTF-8 locale data).
RUN if command -v apt-get >/dev/null 2>&1; then \
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/*; \
else \
# --allowerasing: EL9+ images ship curl-minimal, which conflicts with
# the full curl package - allow dnf to swap it out.
dnf -yq install --allowerasing \
git curl wget ca-certificates zsh tmux python3 \
tar gzip xz findutils shadow-utils glibc-langpack-en && \
dnf clean all; \
fi

# ── Step 2b: Optionally install sudo ──────────────────────────────────────────
# 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.
# nosudo-nonsudoer installs sudo (INSTALL_SUDO=true) but withholds sudoers
# membership below, so detect_sudo() must auto-detect CAN_SUDO=false.
RUN if [ "${GRANT_SUDO}" = "true" ] || [ "${INSTALL_SUDO}" = "true" ]; then \
apt-get -yq update && \
apt-get -yq install --no-install-recommends sudo && \
rm -rf /var/lib/apt/lists/*; \
if command -v apt-get >/dev/null 2>&1; then \
apt-get -yq update && \
apt-get -yq install --no-install-recommends sudo && \
rm -rf /var/lib/apt/lists/*; \
else \
dnf -yq install sudo && dnf clean all; \
fi; \
fi

# ── Step 3: Create non-root user; conditionally grant passwordless sudo ───────
Expand All @@ -94,6 +120,18 @@ WORKDIR /home/user
# 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
# The optional gh_token secret becomes a ~/.curlrc Authorization header for the
# install only (curl reads it automatically); removed afterwards so the image
# carries no credentials.
# mode=0444 (not uid=): Ubuntu 24.04 images ship an 'ubuntu' user at uid 1000,
# pushing 'user' to 1001 - a uid-pinned secret is then unreadable and the
# resulting empty Bearer header turns every API call into a 401.
RUN --mount=type=secret,id=gh_token,mode=0444,required=false \
tok="$(cat /run/secrets/gh_token 2>/dev/null || true)"; \
if [ -n "$tok" ]; then \
printf 'header = "Authorization: Bearer %s"\n' "$tok" > ~/.curlrc; \
fi; \
cd dotfiles && NOSUDO="${NOSUDO_INSTALL}" bash install.sh minimal; \
rc=$?; rm -f ~/.curlrc; exit $rc

CMD ["bash"]
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ curl -fsSL https://raw.githubusercontent.com/YASoftwareDev/dotfiles/master/get.s
NOSUDO=1 ./install.sh workstation
```

**RHEL family (AlmaLinux / Rocky / CentOS Stream / Fedora)** - supported via the
same user-local path: there is no apt, so tools are always fetched as prebuilt
binaries into `~/.local/bin`, with or without sudo. The `NOSUDO=1` override is
unnecessary (no-apt is auto-detected), and a plain user without sudo works out
of the box. The only requirement is that an administrator has preinstalled the
base prerequisites:

```bash
sudo dnf install -y git curl zsh tmux python3 tar gzip xz findutils glibc-langpack-en
```

<details>
<summary>Other install methods</summary>

Expand Down
Loading
Loading