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: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ body:
- type: markdown
attributes:
value: >-
Give **Ubuntu version**, **repro**, and `setup_errors.log` excerpts.
Give **Ubuntu version**, **repro**, and `./scripts/validate.sh` output.
Read **CONTRIBUTING.md** before submitting.

- type: textarea
Expand Down Expand Up @@ -44,7 +44,7 @@ body:
id: logs
attributes:
label: Logs (optional but helpful)
description: Relevant excerpt from repo-root `setup_errors.log` or CI URL.
description: Relevant `./scripts/validate.sh` output or CI URL.
render: plaintext
validations:
required: false
55 changes: 5 additions & 50 deletions .github/workflows/test-runner.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,58 +19,13 @@ jobs:
submodules: recursive

- name: Make scripts executable
run: |
find src/scripts -name '*.sh' -exec chmod +x {} +
run: find src/scripts -name '*.sh' -exec chmod +x {} +

# CI: no GNOME session; config/system/gnome-gsettings.sh exits early.
# CI: no GNOME session; gnome-gsettings.sh exits early.
- name: Run setup scripts
run: |
cd src/scripts
bash master.sh || true

- name: Validate installed tools and apps
run: |
export PATH="${HOME}/.local/bin:/usr/local/bin:${PATH}"
chmod +x scripts/validate-installs.sh
./scripts/validate-installs.sh

- name: Check error log
working-directory: ${{ github.workspace }}
run: |
ERROR_LOG="setup_errors.log"
if [[ -f "$ERROR_LOG" ]] && [[ -s "$ERROR_LOG" ]]; then
FILTERED_LOG=$(mktemp)
re=''
re+='Running kernel seems to be up-to-date|Restarting services|'
re+='Services to be restarted|Service restarts being deferred|'
re+='Flatpak system operation|ConfigureRemote not allowed|'
re+='No containers need to be restarted|No VM guests are running|'
re+='User sessions running outdated binaries|'
re+='No user sessions are running outdated binaries|'
re+='runner @ user manager service|needrestart|'
re+='No services need to be restarted|'
re+='chsh: PAM: Authentication failure|Password:|systemctl restart|'
re+='WARNING: apt does not have a stable CLI|'
re+='Synchronizing state of docker|'
re+='Executing: /usr/lib/systemd|^Cloning into|'
re+='^Updating files:|^Resolving |^Connecting to |'
re+='^HTTP request sent|'
re+='^Length:|^Saving to:|'
re+='^--[0-9]{4}-[0-9]{2}-[0-9]{2}|^[0-9]{4}-[0-9]{2}-[0-9]{2}.*saved|'
re+='^[[:space:]]*[0-9]+K|done\.$|^[[:space:]]*100%|^npm notice|'
re+='% Total|% Received|% Xferd|Average Speed|Dload Upload|'
re+='Time Time Time Current|--:--:--|'
re+='^[[:space:]]*[0-9]+(\.[0-9]+)?%[[:space:]]*$|'
re+='^#+[[:space:]]*[0-9]+(\.[0-9]+)?%[[:space:]]*$|'
re+='^curl:.*progress|'
grep -v -E "$re" "$ERROR_LOG" \
| grep -v '^[[:space:]]*$' >"$FILTERED_LOG" || true
bash master.sh

if [[ -s "$FILTERED_LOG" ]]; then
echo "❌ Errors found:"
cat "$FILTERED_LOG"
rm -f "$FILTERED_LOG"
exit 1
fi
rm -f "$FILTERED_LOG"
fi
- name: Validate installs and config
run: chmod +x scripts/validate.sh && ./scripts/validate.sh
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Log files
setup_errors.log
setup_summary.txt

# Node
Expand Down
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,20 @@ pull requests. Run `npm ci` and the relevant tools when you touch those file typ
### Test workflow

`.github/workflows/test-runner.yaml` runs `src/scripts/master.sh` on `ubuntu-latest`, then
`scripts/validate-installs.sh` (version and presence checks for each installed tool/app).
`scripts/validate.sh` (install binaries/packages and config paths/settings).
GNOME gsettings scripts no-op without an active GNOME session.

## Layout

| Path | Role |
|------|------|
| `src/scripts/lib/env.sh` | `PROJECT_ROOT`, `ERROR_LOG_FILE`, `TEMP_DIR` |
| `src/scripts/lib/env.sh` | `PROJECT_ROOT`, `TEMP_DIR` |
| `src/scripts/lib/run.sh` | `run_script` helper |
| `src/scripts/lib/gnome-session.sh` | Skip GNOME config when not on GNOME |
| `src/scripts/lib/zsh-login.sh` | `.zshrc` pass-cli guard for provisioning |
| `src/scripts/install/<category>/` | Per-package install scripts + `all.sh` |
| `src/scripts/install/packages/*.packages` | Apt package lists (one per line) |
| `src/scripts/lib/apt-packages.sh` | `install_apt_packages_from_file` helper |
| `src/scripts/install/` | `packages/`, `apps/`, `dev/`, `shell/`, `post-install/` |
| `src/scripts/config/<category>/` | Dotfiles/GNOME/system config + `all.sh` |

## Commits
Expand Down
113 changes: 41 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Ubuntu setup scripts

Provisioning for a personal Ubuntu desktop: per-app install scripts under
Provisioning for a personal Ubuntu desktop: install scripts under
`src/scripts/install/`, dotfiles and system config under `src/scripts/config/`,
orchestrated by `master.sh`.

Expand All @@ -12,8 +12,8 @@ bash run-install.sh # install only
bash run-config.sh # config only
```

CI runs `master.sh` on `ubuntu-latest`, then `scripts/validate-installs.sh` to
check that expected binaries and packages are present.
CI runs `master.sh` on `ubuntu-latest`, then `scripts/validate.sh` to confirm
expected binaries/packages and config outcomes (dotfiles paths, UFW, system policy).

## Package manager preference

Expand All @@ -24,79 +24,48 @@ Install scripts prefer, in order:
3. **AppImage** or upstream static/binary releases
4. **snap** (only when no practical alternative)

## What gets installed

### CLI (`install/cli/`)

| Tool | Method |
|------|--------|
| bat, curl, eza, fd-find, git, htop, jq, ripgrep, vim, wget | apt (universe) |
| yazi, lazygit | apt ([debian.griffo.io](https://debian.griffo.io/apt)) |
| btop | apt (Ubuntu ≥ 22.04), else GitHub musl binary, else snap |
| fastfetch | apt (PPA) |
| flatpak + Flathub | apt + flatpak remotes |

### Media (`install/media/`)

| App | Method |
|-----|--------|
| Brave Browser | apt (vendor repo) |
| VLC | apt |
| ffmpeg, ubuntu-restricted-extras | apt |

### Productivity (`install/productivity/`)

| App | Method |
|-----|--------|
| LibreOffice | apt |
| Zoom | `.deb`, else Flatpak, else snap |
| KeePassXC, Redshift, Flameshot | apt |
| balenaEtcher | apt (`.deb` from GitHub releases) |
## Install layout

| Path | Role |
|------|------|
| `install/preflight/` | apt update, essentials (git, curl, universe), timezone |
| `install/packages/*.packages` | One apt package per line; installed by `packages/all.sh` |
| `install/griffo.sh`, `fastfetch.sh`, `btop.sh`, `flatpak.sh` | Repos, PPAs, or fallbacks only where apt lists are not enough |
| `install/apps/` | Vendor apt repos, `.deb`, Flatpak/snap fallbacks |
| `install/dev/` | NodeSource, nvm, LSP language stacks, Docker, Neovim PPA, rustup, gems, pip/npm tools |
| `install/shell/` | Ghostty, Meslo font, Oh My Posh |
| `install/post-install/` | apt maintain, Docker service, completion banner |

### Package lists (`install/packages/`)

| File | Contents |
|------|----------|
| `base.packages` | CLI and security tools (bat, fzf, gh, jq, ripgrep, ufw, nmap, exiftool, …) |
| `shell.packages` | zsh, tmux, fonts, plugins |
| `media.packages` | vlc, ffmpeg, gstreamer |
| `desktop.packages` | GNOME Tweaks, shell extensions |
| `productivity.packages` | LibreOffice, KeePassXC, Redshift, Flameshot |
| `lsp.packages` | Mason LSP runtimes (Go, Ruby, PHP, Lua, …) |
| `lsp-optional.packages` | Julia (skipped when unavailable on apt) |
| `dev.packages` | Neovim, Python |
| `griffo.packages` | yazi, lazygit, lazydocker ([debian.griffo.io](https://debian.griffo.io/apt)) |
| `fastfetch.packages` | fastfetch (PPA) |

### Apps (`install/apps/`)

Brave, Signal, Proton VPN/Pass, Bruno, Zoom, Etcher, OWASP ZAP, ufw-docker,
Hacking git clones — each script handles its own repo or `.deb` when apt lists are
not enough.

### Development (`install/dev/`)

| Tool | Method |
|------|--------|
| Node.js | apt (NodeSource) |
| nvm | upstream install script |
| Python 3, pip, venv, dev headers | apt |
| Vue CLI | npm global |
| Docker CE + Compose | apt (Docker vendor repo) |
| Neovim | apt (PPA) + Python extras |
| gh, shellcheck, git | apt |
| **Bruno** (API client) | apt ([debian.usebruno.com](http://debian.usebruno.com/)), else Flatpak |
| Semgrep | pip (`--user`) |
| Cursor Agent CLI | [cursor.com/install](https://cursor.com/install) |

### Security (`install/security/`)

| Tool | Method |
|------|--------|
| UFW, OpenVPN | apt |
| Proton VPN | apt (vendor `.deb` + packages) |
| Proton Pass (desktop) | vendor `.deb` |
| pass-cli | GitHub release binary |
| Signal Desktop | apt (vendor repo) |
| nmap, exiftool | apt |
| OWASP ZAP | Flatpak, else snap (`--classic`) |
| PayloadsAllTheThings, SecLists | git clone into `~/Hacking` |

### Shell (`install/shell/`)

| Tool | Method |
|------|--------|
| zsh, tmux, powerline | apt |
| zsh-autosuggestions, zsh-syntax-highlighting | apt |
| Font Awesome, Fira Code, Powerline fonts | apt |
| Meslo Nerd Font | GitHub release → `/usr/share/fonts` |
| Oh My Posh | [ohmyposh.dev](https://ohmyposh.dev) → `~/.local/bin` |
| Ghostty | [ghostty-ubuntu](https://github.com/mkasberg/ghostty-ubuntu) install script |
Node.js (NodeSource), nvm, Docker CE + Compose, rustup, Solargraph gem, Semgrep,
Vue CLI, Cursor Agent CLI.

### Preflight & post-install

- apt update/upgrade, essentials (git, curl, universe enabled)
- timezone (Los Angeles)
- Docker service enabled; UFW enabled after rules config
- apt update/upgrade, essentials, universe, timezone (Los Angeles)
- Docker service enabled; UFW rules in `config/security/` (LocalSend, Docker DNS, ufw-docker)

## Explicitly not installed

Expand All @@ -116,7 +85,7 @@ These are **not** provisioned by this repo (remove from old notes or other dotfi

Symlinks and settings from `src/dotfiles` (submodule): Zsh, tmux, Neovim, btop,
fastfetch, Kitty/Alacritty/Ghostty, Git, VS Code `settings.json`, GNOME
gsettings (skipped in CI without a GNOME session), UFW rules, home directory
layout.
gsettings (skipped in CI without a GNOME session), UFW defaults and rules (LocalSend,
Docker DNS, ufw-docker), home directory layout.

See [AGENTS.md](AGENTS.md) for contributor conventions, ShellCheck, and CI details.
93 changes: 93 additions & 0 deletions scripts/lib/validate-common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/bin/bash

FAILURES=0

section() {
printf '\n== %s ==\n' "$1"
}

pass() {
local name="$1"
local detail="${2:-}"
if [[ -n "$detail" ]]; then
printf ' ok %-28s %s\n' "$name" "$detail"
else
printf ' ok %s\n' "$name"
fi
}

fail() {
local name="$1"
local detail="${2:-not found}"
printf ' FAIL %-28s %s\n' "$name" "$detail" >&2
FAILURES=$((FAILURES + 1))
}

version_of() {
local cmd=("$@")
"${cmd[@]}" 2>/dev/null | head -n1 | tr -d '\r' || true
}

check_version() {
local name="$1"
shift
local rc=0
"$@" >/dev/null 2>&1 || rc=$?
if [[ "$rc" -eq 0 ]]; then
pass "$name" "$(version_of "$@")"
else
fail "$name" "expected: $*"
fi
}

check_command() {
local name="$1"
local bin="$2"
if command -v "$bin" >/dev/null 2>&1; then
pass "$name" "$(command -v "$bin")"
else
fail "$name" "command not in PATH: $bin"
fi
}

check_dpkg() {
local name="$1"
local pkg="$2"
if dpkg -s "$pkg" >/dev/null 2>&1; then
pass "$name" "$(dpkg -s "$pkg" 2>/dev/null | awk -F': ' '/^Version:/{print $2; exit}')"
else
fail "$name" "dpkg package missing: $pkg"
fi
}

check_path() {
local name="$1"
local path="$2"
if [[ -e "$path" ]]; then
pass "$name" "$path"
else
fail "$name" "missing path: $path"
fi
}

flatpak_installed() {
local app_id="$1"
flatpak list --columns=application --app 2>/dev/null | grep -Fxq "$app_id" && return 0
flatpak list --user --columns=application --app 2>/dev/null | grep -Fxq "$app_id" && return 0
return 1
}

snap_installed() {
local snap_name="$1"
snap list "$snap_name" 2>/dev/null | grep -q "^${snap_name} "
}

finish_validation() {
local label="$1"
printf '\n'
if [[ "$FAILURES" -gt 0 ]]; then
printf '%s failed: %d check(s).\n' "$label" "$FAILURES" >&2
exit 1
fi
printf '%s passed.\n' "$label"
}
Loading
Loading