From 58648c65f49c105c3fcb1633dcd94956a9f31ab6 Mon Sep 17 00:00:00 2001 From: Ulises Chavarria Date: Mon, 24 Aug 2026 22:56:18 -0700 Subject: [PATCH 1/3] fix(setup): harden provisioning and SSH configuration --- SSH_and_GPG/create_ssh_key.sh | 116 +++++++++++++++++++++++++--------- lib/core.sh | 39 +++++++++++- platforms/macos.sh | 16 +---- 3 files changed, 126 insertions(+), 45 deletions(-) diff --git a/SSH_and_GPG/create_ssh_key.sh b/SSH_and_GPG/create_ssh_key.sh index 69acaac..873722a 100755 --- a/SSH_and_GPG/create_ssh_key.sh +++ b/SSH_and_GPG/create_ssh_key.sh @@ -81,6 +81,11 @@ fi prompt KEY_NAME "Key file name (no path)" "${GIT_HOST%%.*}" +if [[ "$KEY_NAME" == "." || "$KEY_NAME" == ".." || ! "$KEY_NAME" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then + echo "Error: KEY_NAME must be a simple key file name without a path, whitespace, or control characters." >&2 + exit 1 +fi + # ---- Optional passphrase ---- SSH_PASSPHRASE="${SSH_PASSPHRASE:-}" if [[ -z "$SSH_PASSPHRASE" ]]; then @@ -115,6 +120,33 @@ if [[ -n "$SSH_PASSPHRASE" ]]; then fi fi +validate_host_token() { + local value="$1" label="$2" + if [[ -z "$value" ]]; then + printf 'Error: %s cannot be empty.\n' "$label" >&2 + exit 1 + fi + if [[ "$value" == *://* || "$value" == *[[:space:][:cntrl:]]* || "$value" == -* || "$value" == *\** || "$value" == *\?* || "$value" == *\!* || "$value" == */* ]]; then + printf 'Error: %s must be one literal SSH host token, not a URL, wildcard, negated pattern, or value containing whitespace/control characters.\n' "$label" >&2 + exit 1 + fi +} + +if [[ -n "$GIT_HOSTNAME" && -z "$IS_SELF_HOSTED" ]]; then + echo "Error: GIT_HOSTNAME requires IS_SELF_HOSTED=true." >&2 + exit 1 +fi + +validate_host_token "$GIT_HOST" "GIT_HOST" +if [[ -n "$IS_SELF_HOSTED" ]]; then + validate_host_token "$GIT_HOSTNAME" "GIT_HOSTNAME" + GIT_SSH_PORT="${GIT_SSH_PORT:-22}" + if [[ ! "$GIT_SSH_PORT" =~ ^[0-9]+$ ]] || (( GIT_SSH_PORT < 1 || GIT_SSH_PORT > 65535 )); then + echo "Error: GIT_SSH_PORT must be a number from 1 to 65535." >&2 + exit 1 + fi +fi + SSH_DIR="$HOME/.ssh" KEY_PATH="$SSH_DIR/$KEY_NAME" PUB_PATH="$KEY_PATH.pub" @@ -175,38 +207,62 @@ fi touch "$CFG_PATH" chmod 600 "$CFG_PATH" -# Remove any existing block for this host (simple, robust approach). -# This deletes from line "Host " up to the next "Host " line (or EOF). -tmp_cfg="$(mktemp)" -awk -v host="$GIT_HOST" ' - BEGIN {skip=0} - $1=="Host" && $2==host {skip=1; next} - $1=="Host" && skip==1 {skip=0} - skip==0 {print} +managed_begin="# BEGIN create_ssh_key.sh: $KEY_NAME" +legacy_begin='# BEGIN create_ssh_key.sh' +legacy_end='# END create_ssh_key.sh' +managed_end="# END create_ssh_key.sh: $KEY_NAME" +tmp_cfg="$(mktemp "$SSH_DIR/config.XXXXXX")" +trap 'rm -f "$tmp_cfg"' EXIT +awk -v begin="$managed_begin" -v legacy_begin="$legacy_begin" -v legacy_end="$legacy_end" -v end="$managed_end" ' + function flush_block( i) { + for (i = 1; i <= block_lines; i++) print block[i] + block_lines = 0 + } + ($0 == begin || $0 == legacy_begin) { + in_block = 1 + block_lines = 1 + block[block_lines] = $0 + next + } + in_block { + block[++block_lines] = $0 + if ($0 == end || $0 == legacy_end) { + in_block = 0 + block_lines = 0 + } + next + } + {print} + END { + if (in_block) flush_block() + } ' "$CFG_PATH" > "$tmp_cfg" -mv "$tmp_cfg" "$CFG_PATH" -if [[ -n "$IS_SELF_HOSTED" ]]; then - { - echo "" - echo "Host $GIT_HOST" - echo " HostName $GIT_HOSTNAME" - # Port 22 is the SSH default — only emit the line for a non-standard port. - [[ -n "$GIT_SSH_PORT" && "$GIT_SSH_PORT" != "22" ]] && echo " Port $GIT_SSH_PORT" - echo " User git" - echo " AddKeysToAgent $ADD_KEYS_TO_AGENT" - echo " IdentityFile $KEY_PATH" - } >> "$CFG_PATH" -else - { - echo "" - echo "Host $GIT_HOST" - echo " AddKeysToAgent $ADD_KEYS_TO_AGENT" - # macOS keychain optional: - # echo " UseKeychain yes" - echo " IdentityFile $KEY_PATH" - } >> "$CFG_PATH" -fi +managed_block="$(mktemp "$SSH_DIR/config-block.XXXXXX")" +final_cfg="$(mktemp "$SSH_DIR/config.XXXXXX")" +trap 'rm -f "$tmp_cfg" "$managed_block" "$final_cfg"' EXIT +{ + printf '%s\n' "$managed_begin" + if [[ -n "$IS_SELF_HOSTED" ]]; then + printf 'Host %s %s\n' "$GIT_HOST" "$GIT_HOSTNAME" + printf ' HostName %s\n' "$GIT_HOSTNAME" + [[ "$GIT_SSH_PORT" != "22" ]] && printf ' Port %s\n' "$GIT_SSH_PORT" + printf ' User git\n' + else + printf 'Host %s\n' "$GIT_HOST" + fi + printf ' AddKeysToAgent %s\n' "$ADD_KEYS_TO_AGENT" + printf ' IdentityFile %s\n' "$KEY_PATH" + printf '%s\n' "$managed_end" +} > "$managed_block" + +{ + cat "$managed_block" + cat "$tmp_cfg" +} > "$final_cfg" +mv "$final_cfg" "$CFG_PATH" +rm -f "$tmp_cfg" "$managed_block" +trap - EXIT # ---- Show public key ---- echo "" diff --git a/lib/core.sh b/lib/core.sh index 3a8d101..a12b941 100755 --- a/lib/core.sh +++ b/lib/core.sh @@ -237,6 +237,37 @@ run_eval() { fi } +core_prime_sudo() { + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] sudo -v (cache credentials + background keepalive)\n' + return 0 + fi + printf '==> Caching sudo credentials (you may be prompted once)...\n' + if ! sudo -v; then + printf 'error: sudo authentication is required for this setup.\n' >&2 + printf ' Run it from an interactive terminal, or authenticate first with: sudo -v\n' >&2 + return 1 + fi + ( while true; do sudo -n true || true; sleep 60; kill -0 "$$" 2>/dev/null || exit; done ) >/dev/null 2>&1 & +} + +prepare_nvm_environment() { + local npmrc="$HOME/.npmrc" backup tmp + unset NPM_CONFIG_PREFIX npm_config_prefix NPM_CONFIG_GLOBALCONFIG npm_config_globalconfig + [[ -f "$npmrc" ]] || return 0 + if ! grep -Eq '^[[:space:]]*(prefix|globalconfig)[[:space:]]*=' "$npmrc"; then + return 0 + fi + backup="${npmrc}.nvm-preflight.bak" + if [[ ! -e "$backup" ]]; then + cp "$npmrc" "$backup" + fi + tmp="$(mktemp)" + grep -Ev '^[[:space:]]*(prefix|globalconfig)[[:space:]]*=' "$npmrc" > "$tmp" || true + mv "$tmp" "$npmrc" + printf ' removed npm prefix settings incompatible with nvm (backup: %s)\n' "$backup" +} + # npm supply-chain cooldown: refuse to install package versions younger than # NPM_MIN_RELEASE_AGE days. Compromised releases of popular packages (e.g. the # axios RAT, Mar 2026) are typically caught and yanked within hours, so a short @@ -743,6 +774,7 @@ linux_nvm_flow() { configure_pnpm return 0 fi + prepare_nvm_environment if [ ! -d "$HOME/.nvm" ]; then printf '==> Installing nvm...\n' eval "$(custom_cmd nvm)" @@ -829,8 +861,10 @@ desktop_pipx_section() { desktop_pnpm_section() { printf '\n==> Installing pnpm packages...\n' - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + if [[ "$DRY_RUN" == false ]]; then + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + fi export PNPM_HOME="$HOME/.local/share/pnpm" export PATH="$PNPM_HOME/bin:$PATH" if command -v pnpm &>/dev/null; then @@ -879,6 +913,7 @@ linux_main() { else CONFIG_SRC_DIR="$SETUP_ROOT/linux-desktop" fi + core_prime_sudo platform_bootstrap printf '\n' diff --git a/platforms/macos.sh b/platforms/macos.sh index 67232bd..319f0b7 100755 --- a/platforms/macos.sh +++ b/platforms/macos.sh @@ -69,20 +69,9 @@ mac_pipx_install_tier() { mac_install_list "$(pkg_names pipx "$1")" pipx install # Cache sudo credentials once up front. Homebrew's cask/pkg installers each shell # out to `sudo`, so without this a fresh install prompts for the password ~6 -# times. Prime the timestamp once, then refresh it in the background until this -# script exits so every later sudo call reuses it silently. +# times. mac_prime_sudo() { - if [[ "$DRY_RUN" == true ]]; then - printf ' [dry-run] sudo -v (cache credentials + background keepalive)\n' - return 0 - fi - printf '==> Caching credentials (you may be prompted for your password once)...\n' - sudo -v || return 0 - # || true: the subshell inherits set -e, and one failed refresh (timestamp - # revoked mid-run) must not silently kill the keepalive. stdout is redirected - # so a piped run (setup.sh | tee) sees EOF at exit instead of hanging on the - # fd this subshell holds for up to 60s. - ( while true; do sudo -n true || true; sleep 60; kill -0 "$$" 2>/dev/null || exit; done ) >/dev/null 2>&1 & + core_prime_sudo } print_app_store_reminders() { @@ -154,6 +143,7 @@ platform_main() { fi if [[ "$DRY_RUN" == false ]]; then + prepare_nvm_environment export NVM_DIR="$HOME/.nvm" [[ -s "$NVM_DIR/nvm.sh" ]] && \. "$NVM_DIR/nvm.sh" nvm install 'lts/*' From 9235ab205dacd8c308c942eb04ac4f04e2a40e82 Mon Sep 17 00:00:00 2001 From: Ulises Chavarria Date: Mon, 21 Sep 2026 23:54:36 -0700 Subject: [PATCH 2/3] fix(ssh): match self-hosted aliases and hostnames --- .github/workflows/lint.yml | 3 ++ SSH_and_GPG/create_ssh_key.sh | 9 +++- SSH_and_GPG/test-create_ssh_key.sh | 78 ++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 SSH_and_GPG/test-create_ssh_key.sh diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6edce2f..6c8dc83 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -76,6 +76,9 @@ jobs: - name: Test CUPS policy rendering run: bash linux-pi/cups/test-setup.sh + - name: Test SSH config rendering + run: bash SSH_and_GPG/test-create_ssh_key.sh + zsh-syntax: runs-on: ubuntu-latest steps: diff --git a/SSH_and_GPG/create_ssh_key.sh b/SSH_and_GPG/create_ssh_key.sh index 873722a..1446c91 100755 --- a/SSH_and_GPG/create_ssh_key.sh +++ b/SSH_and_GPG/create_ssh_key.sh @@ -241,18 +241,23 @@ awk -v begin="$managed_begin" -v legacy_begin="$legacy_begin" -v legacy_end="$le managed_block="$(mktemp "$SSH_DIR/config-block.XXXXXX")" final_cfg="$(mktemp "$SSH_DIR/config.XXXXXX")" trap 'rm -f "$tmp_cfg" "$managed_block" "$final_cfg"' EXIT +host_patterns="$GIT_HOST" +if [[ -n "$IS_SELF_HOSTED" && "$GIT_HOST" != "$GIT_HOSTNAME" ]]; then + host_patterns+=" $GIT_HOSTNAME" +fi { printf '%s\n' "$managed_begin" if [[ -n "$IS_SELF_HOSTED" ]]; then - printf 'Host %s %s\n' "$GIT_HOST" "$GIT_HOSTNAME" + printf 'Host %s\n' "$host_patterns" printf ' HostName %s\n' "$GIT_HOSTNAME" [[ "$GIT_SSH_PORT" != "22" ]] && printf ' Port %s\n' "$GIT_SSH_PORT" printf ' User git\n' else - printf 'Host %s\n' "$GIT_HOST" + printf 'Host %s\n' "$host_patterns" fi printf ' AddKeysToAgent %s\n' "$ADD_KEYS_TO_AGENT" printf ' IdentityFile %s\n' "$KEY_PATH" + printf ' IdentitiesOnly yes\n' printf '%s\n' "$managed_end" } > "$managed_block" diff --git a/SSH_and_GPG/test-create_ssh_key.sh b/SSH_and_GPG/test-create_ssh_key.sh new file mode 100644 index 0000000..895e3c4 --- /dev/null +++ b/SSH_and_GPG/test-create_ssh_key.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TMP_ROOT="$(mktemp -d)" +trap 'rm -rf "$TMP_ROOT"' EXIT + +STUB_BIN="$TMP_ROOT/bin" +mkdir -p "$STUB_BIN" + +cat > "$STUB_BIN/ssh-keygen" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +key_path="" +while (($#)); do + if [[ "$1" == "-f" ]]; then + key_path="$2" + shift 2 + else + shift + fi +done +printf 'private key\n' > "$key_path" +printf 'public key\n' > "$key_path.pub" +EOF + +cat > "$STUB_BIN/ssh-add" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF + +cat > "$STUB_BIN/ssh" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF + +cat > "$STUB_BIN/git" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$*" == "config --global user.name" || "$*" == "config --global user.email" ]]; then + exit 1 +fi +exit 0 +EOF + +chmod +x "$STUB_BIN"/* + +run_case() { + local case_name="$1" git_host="$2" git_hostname="$3" expected_host="$4" + local case_home="$TMP_ROOT/$case_name" + local config + mkdir -p "$case_home" + + printf '\n n\n' | env \ + HOME="$case_home" \ + PATH="$STUB_BIN:$PATH" \ + SSH_AUTH_SOCK="$case_home/agent.sock" \ + EMAIL='test@example.invalid' \ + KEY_NAME='forgejo' \ + GIT_HOST="$git_host" \ + IS_SELF_HOSTED=true \ + GIT_HOSTNAME="$git_hostname" \ + GIT_SSH_PORT=22 \ + SSH_PASSPHRASE='test-passphrase' \ + AGENT_TIMEOUT=15 \ + bash "$REPO_ROOT/SSH_and_GPG/create_ssh_key.sh" >/dev/null + + config="$case_home/.ssh/config" + grep -Fx "Host $expected_host" "$config" >/dev/null + grep -Fx ' IdentitiesOnly yes' "$config" >/dev/null +} + +run_case distinct-hosts gitserver forgejo.example 'gitserver forgejo.example' +run_case identical-hosts forgejo.example forgejo.example forgejo.example + +printf 'SSH config tests passed.\n' From ca254b0955b22fa1bd1b931d8c7f818d6ad54cd8 Mon Sep 17 00:00:00 2001 From: Ulises Chavarria Date: Tue, 22 Sep 2026 02:36:38 -0700 Subject: [PATCH 3/3] test(setup): cover SSH and preflight behavior --- .github/workflows/lint.yml | 3 + SSH_and_GPG/test-create_ssh_key.sh | 65 ++++++++++++++++---- scripts/test-core-preflight.sh | 95 ++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 scripts/test-core-preflight.sh diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6c8dc83..07b7043 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -79,6 +79,9 @@ jobs: - name: Test SSH config rendering run: bash SSH_and_GPG/test-create_ssh_key.sh + - name: Test setup preflight helpers + run: bash scripts/test-core-preflight.sh + zsh-syntax: runs-on: ubuntu-latest steps: diff --git a/SSH_and_GPG/test-create_ssh_key.sh b/SSH_and_GPG/test-create_ssh_key.sh index 895e3c4..f34d639 100644 --- a/SSH_and_GPG/test-create_ssh_key.sh +++ b/SSH_and_GPG/test-create_ssh_key.sh @@ -4,6 +4,7 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TMP_ROOT="$(mktemp -d)" trap 'rm -rf "$TMP_ROOT"' EXIT +SSH_BIN="$(command -v ssh)" STUB_BIN="$TMP_ROOT/bin" mkdir -p "$STUB_BIN" @@ -39,10 +40,11 @@ cat > "$STUB_BIN/git" <<'EOF' #!/usr/bin/env bash set -euo pipefail -if [[ "$*" == "config --global user.name" || "$*" == "config --global user.email" ]]; then - exit 1 -fi -exit 0 +case "$*" in + "config --global user.name") printf 'Test User\n' ;; + "config --global user.email") printf 'test@example.invalid\n' ;; + *) exit 0 ;; +esac EOF chmod +x "$STUB_BIN"/* @@ -50,10 +52,18 @@ chmod +x "$STUB_BIN"/* run_case() { local case_name="$1" git_host="$2" git_hostname="$3" expected_host="$4" local case_home="$TMP_ROOT/$case_name" - local config - mkdir -p "$case_home" + local config output target count + mkdir -p "$case_home/.ssh" + config="$case_home/.ssh/config" + printf '%s\n' \ + '# BEGIN create_ssh_key.sh' \ + 'Host obsolete.example' \ + ' IdentityFile /obsolete/key' \ + '# END create_ssh_key.sh' \ + 'Host unrelated.example' \ + ' User preserve-me' > "$config" - printf '\n n\n' | env \ + printf '\n' | env \ HOME="$case_home" \ PATH="$STUB_BIN:$PATH" \ SSH_AUTH_SOCK="$case_home/agent.sock" \ @@ -62,14 +72,47 @@ run_case() { GIT_HOST="$git_host" \ IS_SELF_HOSTED=true \ GIT_HOSTNAME="$git_hostname" \ - GIT_SSH_PORT=22 \ + GIT_SSH_PORT=2222 \ SSH_PASSPHRASE='test-passphrase' \ AGENT_TIMEOUT=15 \ bash "$REPO_ROOT/SSH_and_GPG/create_ssh_key.sh" >/dev/null - config="$case_home/.ssh/config" - grep -Fx "Host $expected_host" "$config" >/dev/null - grep -Fx ' IdentitiesOnly yes' "$config" >/dev/null + cp "$config" "$case_home/config.first-run" + + printf 'n\n\n' | env \ + HOME="$case_home" \ + PATH="$STUB_BIN:$PATH" \ + SSH_AUTH_SOCK="$case_home/agent.sock" \ + EMAIL='test@example.invalid' \ + KEY_NAME='forgejo' \ + GIT_HOST="$git_host" \ + IS_SELF_HOSTED=true \ + GIT_HOSTNAME="$git_hostname" \ + GIT_SSH_PORT=2222 \ + SSH_PASSPHRASE='test-passphrase' \ + AGENT_TIMEOUT=15 \ + bash "$REPO_ROOT/SSH_and_GPG/create_ssh_key.sh" >/dev/null + + cmp -s "$config" "$case_home/config.first-run" + count="$(grep -Fxc '# BEGIN create_ssh_key.sh: forgejo' "$config" || true)" + [[ "$count" == 1 ]] + count="$(grep -Fxc "Host $expected_host" "$config" || true)" + [[ "$count" == 1 ]] + ! grep -F 'obsolete.example' "$config" >/dev/null + ! grep -Fx '# BEGIN create_ssh_key.sh' "$config" >/dev/null + ! grep -Fx '# END create_ssh_key.sh' "$config" >/dev/null + grep -Fx 'Host unrelated.example' "$config" >/dev/null + grep -Fx ' User preserve-me' "$config" >/dev/null + + for target in "$git_host" "$git_hostname"; do + output="$("$SSH_BIN" -G -F "$config" "$target" 2>/dev/null)" + grep -Fx "hostname $git_hostname" <<< "$output" >/dev/null + grep -Fx 'user git' <<< "$output" >/dev/null + grep -Fx 'port 2222' <<< "$output" >/dev/null + grep -Fx 'addkeystoagent 900' <<< "$output" >/dev/null + grep -Fx 'identitiesonly yes' <<< "$output" >/dev/null + grep -Fx "identityfile $case_home/.ssh/forgejo" <<< "$output" >/dev/null + done } run_case distinct-hosts gitserver forgejo.example 'gitserver forgejo.example' diff --git a/scripts/test-core-preflight.sh b/scripts/test-core-preflight.sh new file mode 100644 index 0000000..85a218e --- /dev/null +++ b/scripts/test-core-preflight.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +write_stub() { + local name="$1" + shift + printf '%s\n' '#!/usr/bin/env bash' 'set -euo pipefail' "$@" > "$STUBS/$name" + chmod +x "$STUBS/$name" +} + +export SETUP_ROOT="$REPO_ROOT" +# shellcheck source=../lib/core.sh +source "$REPO_ROOT/lib/core.sh" + +nvm_home="$WORK/nvm-home" +mkdir -p "$nvm_home" +printf '%s\n' \ + 'registry=https://registry.npmjs.org/' \ + 'prefix=/legacy/npm' \ + ' globalconfig = /legacy/npmrc' \ + 'min-release-age=7' > "$nvm_home/.npmrc" +cp "$nvm_home/.npmrc" "$WORK/original.npmrc" + +HOME="$nvm_home" +export NPM_CONFIG_PREFIX=/legacy/npm +export npm_config_prefix=/legacy/npm +export NPM_CONFIG_GLOBALCONFIG=/legacy/npmrc +export npm_config_globalconfig=/legacy/npmrc +prepare_nvm_environment >/dev/null + +cmp -s "$nvm_home/.npmrc.nvm-preflight.bak" "$WORK/original.npmrc" \ + || fail 'nvm preflight backup does not match the original .npmrc' +! grep -Eq '^[[:space:]]*(prefix|globalconfig)[[:space:]]*=' "$nvm_home/.npmrc" \ + || fail 'nvm-incompatible settings remain in .npmrc' +grep -Fx 'registry=https://registry.npmjs.org/' "$nvm_home/.npmrc" >/dev/null \ + || fail 'nvm preflight removed an unrelated registry setting' +grep -Fx 'min-release-age=7' "$nvm_home/.npmrc" >/dev/null \ + || fail 'nvm preflight removed the npm release-age setting' +for variable in NPM_CONFIG_PREFIX npm_config_prefix NPM_CONFIG_GLOBALCONFIG npm_config_globalconfig; do + ! declare -p "$variable" >/dev/null 2>&1 || fail "$variable remained set after nvm preflight" +done +cp "$nvm_home/.npmrc" "$WORK/clean.npmrc" +[[ -z "$(prepare_nvm_environment)" ]] || fail 'idempotent nvm preflight produced output' +cmp -s "$nvm_home/.npmrc" "$WORK/clean.npmrc" \ + || fail 'idempotent nvm preflight changed .npmrc' + +STUBS="$WORK/stubs" +mkdir -p "$STUBS" +SUDO_LOG="$WORK/sudo.log" +export SUDO_LOG +write_stub sudo \ + 'printf "%s\n" "$*" >> "$SUDO_LOG"' \ + 'if [[ "${1:-}" == "-v" && "${SUDO_VALIDATE_FAIL:-false}" == true ]]; then exit 1; fi' +write_stub sleep 'exit 1' +PATH="$STUBS:$PATH" + +export DRY_RUN=true +: > "$SUDO_LOG" +dry_run_output="$(core_prime_sudo)" +[[ "$dry_run_output" == *'[dry-run] sudo -v'* ]] \ + || fail 'sudo dry run did not describe credential priming' +[[ ! -s "$SUDO_LOG" ]] || fail 'sudo dry run executed sudo' + +export DRY_RUN=false +: > "$SUDO_LOG" +core_prime_sudo >/dev/null +keepalive_pid="$!" +wait "$keepalive_pid" 2>/dev/null || true +sudo_calls=() +while IFS= read -r call; do sudo_calls+=("$call"); done < "$SUDO_LOG" +[[ "${#sudo_calls[@]}" == 2 ]] || fail "expected two sudo calls, found ${#sudo_calls[@]}" +[[ "${sudo_calls[0]}" == '-v' ]] || fail 'sudo validation was not the first call' +[[ "${sudo_calls[1]}" == '-n true' ]] || fail 'sudo keepalive did not use non-interactive refresh' + +: > "$SUDO_LOG" +export SUDO_VALIDATE_FAIL=true +if core_prime_sudo >/dev/null 2>&1; then + fail 'failed sudo validation did not abort the preflight' +fi +unset SUDO_VALIDATE_FAIL +sudo_calls=() +while IFS= read -r call; do sudo_calls+=("$call"); done < "$SUDO_LOG" +[[ "${#sudo_calls[@]}" == 1 && "${sudo_calls[0]}" == '-v' ]] \ + || fail 'failed sudo validation started the keepalive' + +printf 'core preflight tests passed.\n'