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
13 changes: 13 additions & 0 deletions docs/UPDATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ already installed, the command reports that and exits without changing
anything. Use `--tools-only` to explicitly resynchronize the current release's
tools and configuration regardless of whether a new release is available.

A successful update finishes with one result line naming the version
transition, for example `Selfishell updated: 1.2.10 -> 1.2.14`. Release details
stay on the GitHub Release rather than being reproduced in the CLI, and output
for work that actually changed the environment, warnings, errors, and
`--dry-run` previews are unaffected. Because that result closes the whole
command, declining the tools/configuration confirmation or failing in that
phase ends the run without reporting the version change even though the CLI
release has already switched; `selfishell version` confirms the active release.
`--tools-only` closes with `Selfishell tools and configuration synchronized.`
and no version transition: that phase resynchronizes the release's tools and
configuration whether or not anything changes, so its result does not claim
one.

```sh
selfishell status
selfishell update --yes
Expand Down
61 changes: 52 additions & 9 deletions lib/commands/update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ update_tools_and_configuration() {
local dry_run="$2"
local require_configuration="$3"
local skip_packages="$4"
# 1 when command_update closes with the version transition instead, so this
# phase leaves the final result to it.
local defer_result="$5"
local profile platform ghostty_enabled=0

SELFISHELL_UNCHANGED_COUNT=0

selfishell_initialize_paths
if [[ ! -r "$SELFISHELL_STATE_DIR/profile" ]]; then
if [[ "$require_configuration" == 1 ]]; then
Expand Down Expand Up @@ -75,19 +76,18 @@ update_tools_and_configuration() {
if [[ "$skip_packages" == "0" && "$profile" == "developer" ]]; then
install_neovim_plugins "$dry_run" || return
fi
((SELFISHELL_UNCHANGED_COUNT == 0)) ||
printf '%s%d items unchanged.%s\n' "$SELFISHELL_COLOR_CYAN" "$SELFISHELL_UNCHANGED_COUNT" "$SELFISHELL_COLOR_RESET"
if [[ "$dry_run" == 1 ]]; then
printf '%sTool/configuration dry run complete.%s\n' "$SELFISHELL_COLOR_CYAN" "$SELFISHELL_COLOR_RESET"
else
printf '%sSelfishell tools and configuration updated.%s\n' "$SELFISHELL_COLOR_GREEN" "$SELFISHELL_COLOR_RESET"
elif [[ "$defer_result" == 0 ]]; then
printf '%sSelfishell tools and configuration synchronized.%s\n' "$SELFISHELL_COLOR_GREEN" "$SELFISHELL_COLOR_RESET"
fi
}

update_cli_release() {
local version="$1"
local assume_yes="$2"
local dry_run="$3"
local active=""

if [[ -z "$version" ]]; then
version="$(release_latest_version)" || {
Expand All @@ -99,7 +99,10 @@ update_cli_release() {
cli_error "Invalid semantic version: $version"
return "$SELFISHELL_EXIT_USAGE"
}
if [[ -r "$SELFISHELL_ROOT/VERSION" && "$(<"$SELFISHELL_ROOT/VERSION")" == "$version" ]]; then
# Read before the release switches: --cli-only keeps running in this process
# and is then the only place that knows both ends of the transition.
[[ ! -r "$SELFISHELL_ROOT/VERSION" ]] || active="$(<"$SELFISHELL_ROOT/VERSION")"
if [[ "$active" == "$version" ]]; then
SELFISHELL_CLI_UP_TO_DATE=1
SELFISHELL_CLI_TARGET_VERSION="$version"
return
Expand All @@ -111,6 +114,35 @@ update_cli_release() {
confirm_action "Update Selfishell CLI to $version?" "$assume_yes" 0 || return
release_install "$version"
SELFISHELL_CLI_UPDATED=1
SELFISHELL_CLI_SOURCE_VERSION="$active"
SELFISHELL_CLI_TARGET_VERSION="$version"
}

# The version the running release replaced. release_install rewrites the
# previous-release link to the outgoing version immediately before switching
# `current`, so the continuation below reports the transition without carrying
# any state across its `exec`.
update_replaced_version() {
local previous

release_installation_paths 2>/dev/null || return 0
[[ -L "$SELFISHELL_SHARE_DIR/previous" ]] || return 0
previous="$(readlink "$SELFISHELL_SHARE_DIR/previous")"
printf '%s\n' "${previous##*/}"
}

# The single closing result for a completed version change. errexit ends the
# command before this is reached when a selected phase fails, so it reports
# success only. Release details stay on the GitHub Release.
update_report_version_change() {
local source_version="$1"
local target_version="$2"

if [[ -z "$source_version" || "$source_version" == "$target_version" ]]; then
printf '%sSelfishell updated to %s.%s\n' "$SELFISHELL_COLOR_GREEN" "$target_version" "$SELFISHELL_COLOR_RESET"
return
fi
printf '%sSelfishell updated: %s -> %s%s\n' "$SELFISHELL_COLOR_GREEN" "$source_version" "$target_version" "$SELFISHELL_COLOR_RESET"
}

continue_update_with_new_cli() {
Expand All @@ -133,6 +165,8 @@ command_update() {

SELFISHELL_CLI_UPDATED=0
SELFISHELL_CLI_UP_TO_DATE=0
SELFISHELL_CLI_SOURCE_VERSION=""
SELFISHELL_CLI_TARGET_VERSION=""

while (("$#" > 0)); do
case "$1" in
Expand Down Expand Up @@ -202,9 +236,18 @@ command_update() {

if [[ "$mode" != cli ]]; then
if [[ "$mode" == tools && "$continuation" == 0 ]]; then
update_tools_and_configuration "$assume_yes" "$dry_run" 1 "$skip_packages"
update_tools_and_configuration "$assume_yes" "$dry_run" 1 "$skip_packages" 0
else
update_tools_and_configuration "$assume_yes" "$dry_run" 0 "$skip_packages"
update_tools_and_configuration "$assume_yes" "$dry_run" 0 "$skip_packages" "$continuation"
fi
fi

# A continuation always runs from an installed release; the VERSION guard
# only keeps the internal flag harmless outside one.
if [[ "$continuation" == 1 && -r "$SELFISHELL_ROOT/VERSION" ]]; then
printf '\n'
update_report_version_change "$(update_replaced_version)" "$(<"$SELFISHELL_ROOT/VERSION")"
elif [[ "$mode" == cli && "$SELFISHELL_CLI_UPDATED" == 1 ]]; then
update_report_version_change "$SELFISHELL_CLI_SOURCE_VERSION" "$SELFISHELL_CLI_TARGET_VERSION"
fi
}
3 changes: 2 additions & 1 deletion lib/releases.sh
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ release_install() {
cli_error "Failed to activate Selfishell $version."
return 1
}
printf '%sSelfishell CLI updated to %s.%s\n' "$SELFISHELL_COLOR_GREEN" "$version" "$SELFISHELL_COLOR_RESET"
# command_update owns the one closing result for a completed update, so the
# switch is not announced here.
release_prune_inactive
}
19 changes: 16 additions & 3 deletions tests/managed_install_test.bash
Original file line number Diff line number Diff line change
Expand Up @@ -1870,7 +1870,7 @@ test_update_ghostty_preflight_stops_before_other_resources_change() {
fail "Ghostty preflight failure changed an unrelated managed file's state"
[[ -f "$cache_marker" ]] ||
fail "Ghostty preflight failure ran shell-tool cache cleanup"
! grep -Fq 'Selfishell tools and configuration updated' "$TEST_ROOT/stdout" ||
! grep -Fq 'Selfishell tools and configuration synchronized' "$TEST_ROOT/stdout" ||
fail "Ghostty preflight failure printed a success message"
}

Expand Down Expand Up @@ -1898,7 +1898,7 @@ test_update_zshenv_preflight_stops_before_other_resources_change() {
fail "Zshenv preflight failure changed the Zsh loader block"
[[ "$(<"$XDG_STATE_HOME/selfishell/resources/vimrc.state")" == "$before_vimrc_state" ]] ||
fail "Zshenv preflight failure changed an unrelated managed file's state"
! grep -Fq 'Selfishell tools and configuration updated' "$TEST_ROOT/stdout" ||
! grep -Fq 'Selfishell tools and configuration synchronized' "$TEST_ROOT/stdout" ||
fail "Zshenv preflight failure printed a success message"
}

Expand All @@ -1924,7 +1924,7 @@ test_update_ghostty_preflight_rejects_directory_before_other_resources_change()
[[ -d "$target/keep" ]] || fail "Contents under the rejected Ghostty config directory were lost"
[[ "$(<"$HOME/.zshrc")" == "$before_zshrc" ]] ||
fail "Ghostty directory preflight failure changed the Zsh loader block"
! grep -Fq 'Selfishell tools and configuration updated' "$TEST_ROOT/stdout" ||
! grep -Fq 'Selfishell tools and configuration synchronized' "$TEST_ROOT/stdout" ||
fail "Ghostty directory preflight failure printed a success message"
}

Expand Down Expand Up @@ -2244,6 +2244,19 @@ test_update_tools_only_yes_preserves_modified_file() {
fail "Non-interactive update conflict must not create a conflict backup"
}

test_tools_only_update_reports_its_own_result_without_a_version_transition() {
run_selfishell install --profile minimal --skip-packages --yes >/dev/null

run_selfishell update --tools-only --skip-packages --yes >"$TEST_ROOT/stdout" 2>&1

grep -Fq 'Selfishell tools and configuration synchronized' "$TEST_ROOT/stdout" ||
fail "--tools-only did not report its own result: $(<"$TEST_ROOT/stdout")"
! grep -Fq 'Selfishell updated' "$TEST_ROOT/stdout" ||
fail "--tools-only reported a CLI version transition: $(<"$TEST_ROOT/stdout")"
! grep -Fq 'items unchanged' "$TEST_ROOT/stdout" ||
fail "update still printed the unchanged-items summary: $(<"$TEST_ROOT/stdout")"
}

run_discovered_tests_parallel \
"${SELFISHELL_TEST_JOBS:-8}" \
setup_managed_home \
Expand Down
34 changes: 28 additions & 6 deletions tests/release_bootstrap_test.bash
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ EOF
assert_symlink_to "releases/$version" "$TEST_ROOT/prefix/share/selfishell/current"
[[ ! -e "$TEST_ROOT/prefix/share/selfishell/previous" ]] ||
fail "A forced release-move failure must not create a previous link"
! grep -Fq 'CLI updated to' "$TEST_ROOT/stdout" ||
! grep -Fq 'Selfishell updated' "$TEST_ROOT/stdout" ||
fail "A forced release-move failure printed a success message"
[[ ! -d "$TEST_ROOT/prefix/share/selfishell/releases/0.2.3" ]] ||
fail "A forced release-move failure must not leave a partial release directory"
Expand Down Expand Up @@ -808,7 +808,7 @@ EOF

((status != 0)) || fail "A forced activation-link failure should propagate as an error"
assert_symlink_to "releases/$version" "$TEST_ROOT/prefix/share/selfishell/current"
! grep -Fq 'CLI updated to' "$TEST_ROOT/stdout" ||
! grep -Fq 'Selfishell updated' "$TEST_ROOT/stdout" ||
fail "A forced activation-link failure printed a success message"
[[ -d "$TEST_ROOT/prefix/share/selfishell/releases/0.2.3" ]] ||
fail "The downloaded release directory should still be usable for a retry"
Expand All @@ -819,7 +819,7 @@ EOF

test_default_update_skips_missing_configuration_and_updates_cli() {
local output
local cli_line skip_line
local result_line skip_line
local version

version="$RELEASE_FIXTURE_VERSION"
Expand All @@ -828,10 +828,32 @@ test_default_update_skips_missing_configuration_and_updates_cli() {
output="$("$TEST_ROOT/prefix/bin/selfishell" update --version 0.2.3 --yes)"
[[ "$output" == *'skipping tools and configuration'* ]] ||
fail "Default update did not skip an uninstalled configuration"
cli_line="$(printf '%s\n' "$output" | awk '/CLI updated to/ { print NR; exit }')"
# The continuation runs from the new release and learns the version it
# replaced from the previous-release link, so the source version here also
# proves that link survived the switch.
[[ "$output" == *"Selfishell updated: $version -> 0.2.3"* ]] ||
fail "Default update did not report the version transition: $output"
result_line="$(printf '%s\n' "$output" | awk '/Selfishell updated: / { print NR; exit }')"
skip_line="$(printf '%s\n' "$output" | awk '/skipping tools and configuration/ { print NR; exit }')"
[[ -n "$cli_line" && -n "$skip_line" && "$cli_line" -lt "$skip_line" ]] ||
fail "Default update did not continue with the new CLI after switching releases"
[[ -n "$result_line" && -n "$skip_line" && "$skip_line" -lt "$result_line" ]] ||
fail "Default update did not report its result after continuing with the new CLI"
assert_symlink_to 'releases/0.2.3' "$TEST_ROOT/prefix/share/selfishell/current"
}

test_cli_only_update_reports_the_version_transition_once() {
local output occurrences version

version="$RELEASE_FIXTURE_VERSION"
run_bootstrap --version "$version" >/dev/null

output="$("$TEST_ROOT/prefix/bin/selfishell" update --cli-only --version 0.2.3 --yes)"
[[ "$output" == *"Selfishell updated: $version -> 0.2.3"* ]] ||
fail "--cli-only did not report the version transition: $output"
occurrences="$(printf '%s\n' "$output" | grep -c 'Selfishell updated' || true)"
[[ "$occurrences" == 1 ]] ||
fail "--cli-only reported the update $occurrences times: $output"
[[ "$output" != *'CLI updated to'* ]] ||
fail "--cli-only still printed the release-installation success line: $output"
assert_symlink_to 'releases/0.2.3' "$TEST_ROOT/prefix/share/selfishell/current"
}

Expand Down