From 6dfaf1376f58490c283f2aadbf8cb808e2f32930 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 16 Aug 2026 15:30:40 -0700 Subject: [PATCH] Make the linux host-setup scripts refuse more than one action --- TODO.md | 8 +++++--- host-setup/bootstrap.sh | 29 +++++++++++++++++++---------- host-setup/linux/install-tools.sh | 23 +++++++++++++++-------- host-setup/linux/setup-github.sh | 15 ++++++++++++--- host-setup/linux/upgrade-host.sh | 17 +++++++++++++---- host-setup/windows/README.md | 1 - 6 files changed, 64 insertions(+), 29 deletions(-) diff --git a/TODO.md b/TODO.md index 5081b1cf..e698c9ac 100644 --- a/TODO.md +++ b/TODO.md @@ -460,10 +460,11 @@ Three findings raised while writing [`host-setup/windows/`][host-setup-windows], - **Align the Linux scripts onto "name one action" instead of "the last one given wins".** Overwriting `MODE` in the arg loop discards an intent silently, and it discards it in the dangerous direction: `--report --install` drops the safe action and keeps the one that changes the host. - **Blocked by** - Nothing. - - **Issue** - [#673][issue-673]. - - **Checked** - `main` at `1d5b076` on 2026-08-11, where all three scripts document last-wins, no documented example passes two actions, `bootstrap.sh` passes exactly one per `run_tool` call, and no test asserts the behavior. + - **Issue** - [#673][issue-673], broadened by [#767][issue-767] to carry the loader as well, which is what this change ships. + - **Checked** - `develop` at `63d244b` on 2026-08-16, where all four scripts document last-wins, no documented example passes two actions, `bootstrap.sh` passes exactly one per `run_tool` call, and no test asserts the behavior. - **Settled** - The Windows tooling already refuses this way. That began as a constraint, since a PowerShell `param()` block records which switches were given and not their order, and the constraint produced the better behavior. - - **Open** - Nothing about the change itself, which is three `usage()` heredocs and three `parse_args()` bodies. The decision is only whether the fleet wants the stricter contract, and taking it deletes the differences-table row in [`host-setup/windows/README.md`][host-setup-windows] rather than leaving a permanent divergence. + - **Settled** - The change is four `usage()` heredocs and four `parse_args()` bodies now that `bootstrap.sh` carries the same contract, and taking it deletes the differences-table row in [`host-setup/windows/README.md`][host-setup-windows] rather than leaving a permanent divergence. + - **Open** - Nothing. ### Neither Host Bootstrap Has Run Against a Truly Fresh Host @@ -625,6 +626,7 @@ Nothing is awaiting close today. [#578][issue-578] was the last entry here and c [issue-672]: https://github.com/ptr727/ProjectTemplate/issues/672 [issue-673]: https://github.com/ptr727/ProjectTemplate/issues/673 [issue-729]: https://github.com/ptr727/ProjectTemplate/issues/729 +[issue-767]: https://github.com/ptr727/ProjectTemplate/issues/767 diff --git a/host-setup/bootstrap.sh b/host-setup/bootstrap.sh index 3a184a84..75d0ad42 100755 --- a/host-setup/bootstrap.sh +++ b/host-setup/bootstrap.sh @@ -40,7 +40,7 @@ Stands a host up: upgrades its packages, installs the host tools, and configures Fetches this repository and runs the tooling from that tree, so the tools and the rules that describe them come from one revision rather than from whatever a host happens to hold. -Actions, the last one given wins, default --report on a terminal is the menu: +Actions, name one, default --report on a terminal is the menu: -r, --report Report what each tool would do, change nothing --host Share the sudo cache, upgrade packages, install the tools, configure git and GitHub, install the skills @@ -246,17 +246,19 @@ menu() { # --- Entry --- parse_args() { + local -a actions=() + while [[ $# -gt 0 ]]; do case "$1" in - -r | --report) MODE="report" ;; - --host) MODE="host" ;; - --dev) MODE="dev" ;; - --upgrade) MODE="upgrade" ;; - --tools) MODE="tools" ;; - --github) MODE="github" ;; - --skills) MODE="skills" ;; - --sudo) MODE="sudo" ;; - --release) MODE="release" ;; + -r | --report) actions+=(report) ;; + --host) actions+=(host) ;; + --dev) actions+=(dev) ;; + --upgrade) actions+=(upgrade) ;; + --tools) actions+=(tools) ;; + --github) actions+=(github) ;; + --skills) actions+=(skills) ;; + --sudo) actions+=(sudo) ;; + --release) actions+=(release) ;; -y | --yes) ASSUME_YES=true ;; -n | --dry-run) DRY_RUN=true ;; --keep) KEEP=true ;; @@ -281,6 +283,13 @@ parse_args() { esac shift done + + # The order the actions were given is not a contract, so more than one is a refusal rather than the last one winning. + if ((${#actions[@]} > 1)); then + die "More than one action given (${actions[*]}), name one" + fi + [[ ${#actions[@]} -eq 1 ]] && MODE="${actions[0]}" + return 0 } main() { diff --git a/host-setup/linux/install-tools.sh b/host-setup/linux/install-tools.sh index 9043cb82..f17d5df9 100755 --- a/host-setup/linux/install-tools.sh +++ b/host-setup/linux/install-tools.sh @@ -68,7 +68,7 @@ Usage: install-tools.sh [options] [tool ...] Installs the host tools the fleet's repositories expect, from upstream where the distro package trails upstream. With no tool named, every managed tool is selected. -Actions, the last one given wins, default --report: +Actions, name one, default --report: -r, --report Report installed and available versions, change nothing -i, --install Install what is missing, leave an installed tool at its version -u, --upgrade Install what is missing and upgrade what is behind @@ -1257,15 +1257,15 @@ configure_sudo_timestamp() { # --- Entry --- parse_args() { - local -a requested=() + local -a requested=() actions=() while [[ $# -gt 0 ]]; do case "$1" in - -r | --report) MODE="report" ;; - -i | --install) MODE="install" ;; - -u | --upgrade) MODE="upgrade" ;; - -l | --list) MODE="list" ;; - --sudo-timestamp) MODE="sudo-timestamp" ;; + -r | --report) actions+=(report) ;; + -i | --install) actions+=(install) ;; + -u | --upgrade) actions+=(upgrade) ;; + -l | --list) actions+=(list) ;; + --sudo-timestamp) actions+=(sudo-timestamp) ;; -n | --dry-run) DRY_RUN=true ;; -y | --yes) ASSUME_YES=true ;; -o | --optional) WITH_OPTIONAL=true ;; @@ -1288,7 +1288,13 @@ parse_args() { shift done - # Checked against the action that won rather than inside the loop, since the last action given is the one that runs. + # The order the actions were given is not a contract, so more than one is a refusal rather than the last one winning. + if ((${#actions[@]} > 1)); then + die "More than one action given (${actions[*]}), name one" + fi + [[ ${#actions[@]} -eq 1 ]] && MODE="${actions[0]}" + + # The sudo-timestamp refusal needs the whole set of tool names a run asked for, so it is checked after the loop rather than inside it. if [[ $MODE == "sudo-timestamp" && ${#requested[@]} -gt 0 ]]; then die "--sudo-timestamp changes the host rather than a tool, so it takes no tool, and \"${requested[*]}\" names one" fi @@ -1307,6 +1313,7 @@ parse_args() { else SELECTED=("${TOOLS[@]}") fi + return 0 } main() { diff --git a/host-setup/linux/setup-github.sh b/host-setup/linux/setup-github.sh index f60b803f..a8e87e08 100755 --- a/host-setup/linux/setup-github.sh +++ b/host-setup/linux/setup-github.sh @@ -59,7 +59,7 @@ Usage: setup-github.sh [options] Sets up git and GitHub on this host: the SSH key, the git configuration, and commit signing. -Actions, the last one given wins, default --status: +Actions, name one, default --status: -s, --status Report what is set up and what is not, change nothing -c, --configure Create the key, apply the configuration, and check both registrations -h, --help Show this help @@ -594,10 +594,12 @@ configure() { # --- Entry --- parse_args() { + local -a actions=() + while [[ $# -gt 0 ]]; do case "$1" in - -s | --status) MODE="status" ;; - -c | --configure) MODE="configure" ;; + -s | --status) actions+=(status) ;; + -c | --configure) actions+=(configure) ;; -n | --dry-run) DRY_RUN=true ;; -y | --yes) ASSUME_YES=true ;; --name) @@ -623,6 +625,13 @@ parse_args() { esac shift done + + # The order the actions were given is not a contract, so more than one is a refusal rather than the last one winning. + if ((${#actions[@]} > 1)); then + die "More than one action given (${actions[*]}), name one" + fi + [[ ${#actions[@]} -eq 1 ]] && MODE="${actions[0]}" + return 0 } main() { diff --git a/host-setup/linux/upgrade-host.sh b/host-setup/linux/upgrade-host.sh index 910ff659..5b0cb34e 100755 --- a/host-setup/linux/upgrade-host.sh +++ b/host-setup/linux/upgrade-host.sh @@ -45,7 +45,7 @@ Usage: upgrade-host.sh [options] Upgrades this host. Packages within the current release are routine; the release itself is a separate action, and is refused on Proxmox and on a distribution this script does not know. -Actions, the last one given wins, default --packages: +Actions, name one, default --packages: -s, --status Report the host, what is upgradable, and what release it could move to -p, --packages Upgrade the packages of the current release -r, --release Upgrade the packages, then upgrade to the next release @@ -585,11 +585,13 @@ upgrade() { # --- Entry --- parse_args() { + local -a actions=() + while [[ $# -gt 0 ]]; do case "$1" in - -s | --status) MODE="status" ;; - -p | --packages) MODE="packages" ;; - -r | --release) MODE="release" ;; + -s | --status) actions+=(status) ;; + -p | --packages) actions+=(packages) ;; + -r | --release) actions+=(release) ;; -n | --dry-run) DRY_RUN=true ;; -y | --yes) ASSUME_YES=true ;; -h | --help) @@ -600,6 +602,13 @@ parse_args() { esac shift done + + # The order the actions were given is not a contract, so more than one is a refusal rather than the last one winning. + if ((${#actions[@]} > 1)); then + die "More than one action given (${actions[*]}), name one" + fi + [[ ${#actions[@]} -eq 1 ]] && MODE="${actions[0]}" + return 0 } main() { diff --git a/host-setup/windows/README.md b/host-setup/windows/README.md index 8e049c7d..9861cd15 100644 --- a/host-setup/windows/README.md +++ b/host-setup/windows/README.md @@ -94,7 +94,6 @@ This moved a boundary the tool used to hold: WSL used to be read-only here, and | --- | --- | --- | | `upgrade-host.sh --release` moves to the next distribution release | no peer | Windows Update owns a feature update, and an action pretending to drive one is the one thing this must not carry | | `install-tools.sh` carries four functions per tool | `install-tools.ps1` carries one registry record per tool | Every source is `winget`, so the per-tool variation those functions exist for does not arise | -| Actions, the last one given wins | Actions, name one | A `param()` block records which switches were given and not their order, and refusing beats silently discarding an intent | | `git-restore-mtime` is managed | not managed | The spec declares it not applicable on Windows, since it serves a Linux deploy path | | `install-tools.sh` refuses docker entirely inside a WSL *distribution* | `install-tools.ps1` checks the WSL *platform* version before installing docker | A WSL distribution takes docker only from Docker Desktop's own WSL integration, and Windows needs WSL2 present for Docker Desktop's own backend | | `sudo` re-runs a command as root | nothing elevates | `winget` raises UAC per installer, which is the path with the fewest failures |