From f8bb57f6669ffa8e30b130e9e3479a70ea51f91c Mon Sep 17 00:00:00 2001 From: alvinlollo Date: Thu, 8 Oct 2026 14:19:32 +1100 Subject: [PATCH] Add pre-push gate, container smoke test, README docs; drop vendored gum.md - hooks/pre-push: bash -n + shellcheck -S warning -e SC1090 on every tracked *.sh; fails closed when shellcheck is missing, bypassable with --no-verify; enable with 'git config core.hooksPath hooks' (documented) - tests/smoke.sh: manual docker/podman Arch-container run of bat.sh, zsh.sh and fish.sh with assertions (idempotent bat config, timestamped zsh backups, fisher actually installed in fish); LazyVim.sh opt-in. This catches the inverted-condition/quoting class shellcheck cannot. fish.sh gains a SKIP_END4_SETUP=1 test hook to skip end-4's installer - README: scripts table (documents installbeta.sh vs install.sh and why SecureBoot.sh is not in the menu), apt full-upgrade note, development section; claims audited against current behavior (gum wording already correct, no whiptail/selection claims remain) - remove untracked vendored gum.md (reference only, per decision) --- README.md | 29 +++++++++++ fish.sh | 16 ++++-- hooks/pre-push | 41 +++++++++++++++ tests/smoke.sh | 137 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 5 deletions(-) create mode 100755 hooks/pre-push create mode 100755 tests/smoke.sh diff --git a/README.md b/README.md index eaaca09..586fe85 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,35 @@ For installing zsh and themes (also works on Debian based systems): curl -fsSL https://raw.githubusercontent.com/alvinlollo/Single-install-script/refs/heads/main/zsh.sh | bash ``` +> **Note:** on Debian-based systems the installer runs `apt full-upgrade -y` as part of its prerequisites. On Arch it installs prerequisites through shelly without upgrading the whole system. + +## Scripts + +| Script | Purpose | +| -------------- | ----------------------------------------------------------------------- | +| `install.sh` | Main menu (gum) that dispatches to the scripts below. | +| `installbeta.sh` | Same menu, but logs every step and records failures instead of aborting, then exits non-zero if anything failed. This is the next `install.sh` — once tested it replaces it. | +| `zsh.sh` | zsh + oh-my-zsh + plugins and my config (also works on Debian). | +| `fish.sh` | fish + Fisher plugins + end-4 dotfiles config. | +| `LazyVim.sh` | My LazyVim neovim config. | +| `bat.sh` | bat + Catppuccin Mocha theme + MANPAGER setup. | +| `SecureBoot.sh`| **Not in the menu** (intentional): manual UEFI-only Secure Boot setup with `sbctl`. It verifies every sign target and asks before enrolling keys — read it before running. | + +## Development + +Enable the pre-push gate once per clone (runs `bash -n` and `shellcheck` on every tracked `*.sh`): + +```bash +git config core.hooksPath hooks +``` + +There is also a manual end-to-end smoke test that runs the installers in a throwaway Arch container (needs docker or podman, network access; **not** part of the hook): + +```bash +tests/smoke.sh # bat.sh, zsh.sh, fish.sh +tests/smoke.sh LazyVim.sh # opt in to the heavy LazyVim run +``` + ## Zsh configuration My Zsh configuration includes the following plugins run through [oh-my-zsh](https://ohmyz.sh/): git, [zsh-history-substring-search](https://github.com/zsh-users/zsh-history-substring-search), [zsh-autosuggestions](https://github.com/zsh-users/zsh-autosuggestions), [zsh-autocomplete](https://github.com/marlonrichert/zsh-autocomplete), [fzf-zsh-plugin](https://github.com/unixorn/fzf-zsh-plugin) and [zsh-syntax-highlighting](https://github.com/zsh-users/zsh-syntax-highlighting.git) diff --git a/fish.sh b/fish.sh index 3d5d236..42ab709 100755 --- a/fish.sh +++ b/fish.sh @@ -98,13 +98,19 @@ if [ -n "$SCRIPT_DIR" ] && [ -d "$SCRIPT_DIR/configs" ]; then done fi -# Run end-4 setup script with flags to skip unnecessary components +# Run end-4 setup script with flags to skip unnecessary components. +# SKIP_END4_SETUP=1 is a test hook used by tests/smoke.sh to validate this +# script's own logic without running end-4's heavyweight installer. echo "WARNING: the end-4 installer runs with --force and --skip-backup;" echo "it may overwrite an existing Hyprland configuration without a backup." -( - cd "$WORK_DIR/dots-hyprland" - ./setup install --force --skip-plasmaintg --skip-backup --skip-quickshell --skip-hyprland --skip-hyprland-entry -) +if [ "${SKIP_END4_SETUP:-0}" = "1" ]; then + echo "(SKIP_END4_SETUP=1 - skipping the end-4 installer)" +else + ( + cd "$WORK_DIR/dots-hyprland" + ./setup install --force --skip-plasmaintg --skip-backup --skip-quickshell --skip-hyprland --skip-hyprland-entry + ) +fi # Resolve a config file: prefer this repo's copy, fall back to the end-4 clone cfg_src() { diff --git a/hooks/pre-push b/hooks/pre-push new file mode 100755 index 0000000..7412ec0 --- /dev/null +++ b/hooks/pre-push @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Pre-push gate: syntax-check and shellcheck every tracked shell script. +# Enable once per clone with: git config core.hooksPath hooks +# Bypass a single push with: git push --no-verify +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +mapfile -t scripts < <(git ls-files '*.sh') +if [ "${#scripts[@]}" -eq 0 ]; then + echo "pre-push: no tracked shell scripts found" >&2 + exit 0 +fi + +status=0 + +for f in "${scripts[@]}"; do + if ! bash -n "$f"; then + echo "pre-push: bash -n FAILED for $f" >&2 + status=1 + fi +done + +if ! command -v shellcheck >/dev/null; then + echo "pre-push: shellcheck is not installed - the gate cannot run." >&2 + echo " install it (e.g. 'pacman -S shellcheck' or 'apt install shellcheck')" >&2 + echo " or bypass this once with: git push --no-verify" >&2 + exit 1 +fi + +# SC1090 (can't follow non-constant source) is excluded: every 'source ~/.bashrc' +# style call in these scripts triggers it and it is not actionable. +if ! shellcheck -S warning -e SC1090 "${scripts[@]}"; then + echo "pre-push: shellcheck reported problems (see above)" >&2 + status=1 +fi + +if [ "$status" -eq 0 ]; then + echo "pre-push: all ${#scripts[@]} shell scripts pass bash -n + shellcheck" +fi +exit "$status" diff --git a/tests/smoke.sh b/tests/smoke.sh new file mode 100755 index 0000000..4573896 --- /dev/null +++ b/tests/smoke.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# Manual end-to-end smoke test. NOT part of the pre-push hook - run by hand: +# +# tests/smoke.sh # default set: bat.sh zsh.sh fish.sh +# tests/smoke.sh LazyVim.sh # opt in to the heavy LazyVim run +# tests/smoke.sh bat.sh # run specific scripts only +# +# Runs each script inside a throwaway Arch container with a stubbed 'sudo' +# (the container is already root). Needs docker or podman and network access. +# This is what catches the bug class shellcheck cannot: inverted conditions, +# swapped shell branches, quoting that collapses when written to a file. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +if command -v docker >/dev/null 2>&1; then + RUNTIME=docker +elif command -v podman >/dev/null 2>&1; then + RUNTIME=podman +else + echo "error: neither docker nor podman found - cannot run the smoke test." >&2 + exit 1 +fi + +DEFAULT_SCRIPTS=(bat.sh zsh.sh fish.sh) +if [ "$#" -gt 0 ]; then + SCRIPTS=("$@") +else + SCRIPTS=("${DEFAULT_SCRIPTS[@]}") +fi + +# Validate arguments up front so a typo does not run in a container first +for s in "${SCRIPTS[@]}"; do + if [ ! -f "$ROOT/$s" ]; then + echo "error: $s not found in $ROOT" >&2 + exit 1 + fi +done + +# Everything inside the container: +# - copy the read-only mount so scripts that write to their own directory +# (fish.sh syncing configs/) cannot mutate the host checkout +# - stub 'sudo' -> exec (container already runs as root) +# - pacman -Sy so package lookups work on a fresh image +# - HOME=/root keeps every script writing inside the container +container_run() { + local body="$1" + "$RUNTIME" run --rm \ + -v "$ROOT:/src:ro" \ + -e HOME=/root \ + archlinux:latest \ + bash -c " +set -euo pipefail +cp -r /src /work +cd /work +printf '#!/bin/sh\nexec \"\$@\"\n' > /usr/local/bin/sudo +chmod +x /usr/local/bin/sudo +pacman -Sy --noconfirm >/dev/null 2>&1 || true +$body +" +} + +declare -a RESULTS=() +overall=0 + +run_test() { + local script="$1" body="$2" + echo "" + echo "=== smoke: $script ===" + if container_run "$body"; then + echo "=== PASS: $script ===" + RESULTS+=("PASS $script") + else + echo "=== FAIL: $script (exit $?)" >&2 + RESULTS+=("FAIL $script") + overall=1 + fi +} + +for script in "${SCRIPTS[@]}"; do + case "$script" in + bat.sh) + run_test "$script" ' + bash bat.sh --skip-watermark + command -v bat >/dev/null || { echo "bat not on PATH after install"; exit 1; } + grep -qxF -- "--theme=\"Catppuccin Mocha\"" "$(bat --config-dir)/config" \ + || { echo "theme line missing from bat config:"; cat "$(bat --config-dir)/config" 2>/dev/null; exit 1; } + [ -s "$(bat --config-dir)/themes/Catppuccin Mocha.tmTheme" ] \ + || { echo "theme file was not downloaded"; exit 1; } + # re-run must be idempotent (no duplicate config lines) + bash bat.sh --skip-watermark + [ "$(grep -cF -- "--theme=" "$(bat --config-dir)/config")" -eq 1 ] \ + || { echo "bat config is not idempotent"; exit 1; } + ' + ;; + zsh.sh) + run_test "$script" ' + bash zsh.sh --skip-watermark + [ -f /root/.zshrc ] || { echo ".zshrc missing"; exit 1; } + [ -d /root/.oh-my-zsh/custom/plugins/zsh-autosuggestions ] || { echo "plugins missing"; exit 1; } + # second run must rotate the backup, not overwrite it + bash zsh.sh --skip-watermark + [ "$(find /root -maxdepth 1 -name ".zshrc.bak-*" | wc -l)" -ge 2 ] \ + || { echo "backup rotation failed"; exit 1; } + ' + ;; + fish.sh) + run_test "$script" ' + export SKIP_END4_SETUP=1 + bash fish.sh --skip-watermark + [ -f /root/.config/fish/config.fish ] || { echo "fish config not installed"; exit 1; } + fish -c "type -q fisher" || { echo "fisher not installed in fish"; exit 1; } + # the fixed prerequisite check must have installed fish itself + command -v fish >/dev/null || { echo "fish missing"; exit 1; } + ' + ;; + LazyVim.sh) + run_test "$script" ' + bash LazyVim.sh + [ -d /root/.config/nvim ] || { echo "nvim config not cloned"; exit 1; } + command -v node >/dev/null || { echo "no node after install"; exit 1; } + # timestamped backup only if a config existed; none here, so just assert + # the clone exists and nothing is named plain nvim.bak + [ ! -e /root/.config/nvim.bak ] || { echo "legacy .bak name used"; exit 1; } + ' + ;; + *) + echo "error: unknown smoke test '$script' (known: bat.sh zsh.sh fish.sh LazyVim.sh)" >&2 + exit 1 + ;; + esac +done + +echo "" +echo "=== smoke summary ===" +printf '%s\n' "${RESULTS[@]}" +exit "$overall"