Skip to content

feat: add release notifications and interactive updates - #468

Merged
nieomylnieja merged 38 commits into
mainfrom
add-new-feature-and-new-version-notifications
Sep 24, 2026
Merged

nieomylnieja merged 38 commits into
mainfrom
add-new-feature-and-new-version-notifications

Conversation

@nieomylnieja

@nieomylnieja nieomylnieja commented May 21, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

Help users keep sloctl up to date without letting optional release notifications or failed updates stop the requested command.

Summary

  • Added a cached release notice with inline highlights and a release link. Eligible interactive sessions receive the same notice for every installation method.
  • Added Update, Skip, and Skip until next version for recognized Homebrew and Go installations on Linux and macOS. Homebrew detection resolves brew through PATH and checks that it owns the running executable.
  • Made Windows notifications noninteractive in every terminal. Windows displays the release information and continues the requested command without reading input.
  • Suppressed release checks for shell completion generation and tab completion so they cannot block shell startup or request update input.
  • Preserved the terminal foreground for ordinary form text so choices remain readable when background detection is unavailable.
  • Defaulted to Skip and recovered from fetch, cache, prompt, and updater errors. Updater output goes to stderr to preserve command stdout. A successful update exits without running the requested command.

Testing

Results and screenshots

The screenshots show actual terminal output from a v1.0.0 build and a local v1.1.0 release fixture. The Linux and macOS prompt captures include the theme fix in 7a44da8; the other captures use f27e9ec. These checks need no Nobl9 credentials.

Platform Result
Linux, NixOS x86-64 The Go installation showed all three choices on light and dark backgrounds. Enter selected Skip and the version command exited 0 in both palettes. A real Go download failure also preserved the version output and exit status 0.
Windows 11 Pro x86-64, Dockur MSYS2 mintty with ConPTY disabled printed the notice, highlights, release link, and version without input. Exit status was 0. This exercises the terminal that previously hung.
macOS 15.8 (24H23) x86-64, Dockur Terminal showed all three Go-install choices on light and dark backgrounds. Enter selected Skip and exited 0 in both palettes. An unrecognized installation printed the same highlights without input. A real Go download failure preserved the version output and exit status 0.

Linux: light background, default Skip

Linux update prompt with inline highlights and Skip selected

Linux: dark background, readable update choices

Linux dark terminal with all three update choices readable

Linux: failed Go update, original command recovered

Linux Go download failure followed by version output and exit status zero

Windows: notice and command completion without input

Windows notice with inline highlights and successful command completion

macOS: light background, default Skip

macOS Terminal update prompt with inline highlights and Skip selected

macOS: dark background, readable update choices

macOS dark terminal with all three update choices readable

macOS: unrecognized installation, notice without input

macOS Terminal notice with inline highlights and successful command completion

macOS: failed Go update, original command recovered

macOS Terminal Go download failure followed by version output and exit status zero

Automated coverage

  • The native Linux notification suite passed all 17 tests. It exercised update selection, Skip, EOF, invalid input, installation mismatches, Homebrew command resolution, and JSON stdout preservation after updater failure.
  • Accessible-mode CLI tests verified typo retry and Skip on empty input or bare EOF. A numeric choice followed by EOF selected that action. Invalid input followed by EOF triggered Huh's panic, which sloctl recovered from before completing the requested command.
  • CLI regression tests reproduced Bash startup and both dynamic completion requests with terminal input and stderr. They completed without input, notices, or release requests. Before the fix, both cases timed out at the update prompt.
  • Shared Bats fixtures check identical highlights with and without update choices. Local fixture tests cover fetch, malformed-response, cache, and updater failures.
  • The Windows regression tests cover notice-only behavior in legacy terminals, including accessible mode, and native consoles. They check that the command completes without input.

Manual testing: Linux laptop

Run these steps directly on your Linux laptop. This path needs no VM or container.

Manual setup

  1. Check out this PR. Open two interactive Bash terminals in the repository root.

  2. Use Go 1.26.6 or newer, Git, GNU Make, and Python 3 in both terminals. On NixOS, run this in each terminal:

    nix-shell -p go gnumake python3 git

    Keep stdin and stderr attached to the terminal when running sloctl.

  3. In terminal 1, start the local release server and leave it running:

    export RELEASE_SERVER_BODY_FILE="$PWD/test/inputs/notifications/release-bodies/features-with-details.md"
    python3 test/inputs/notifications/release_server.py "$(mktemp)" 38080
  4. In terminal 2, install the PR binary into a temporary Go installation directory:

    unset CI SLOCTL_NO_NOTIFICATIONS SLOCTL_ACCESSIBLE_MODE
    sloctl_test_dir="$(mktemp -d)"
    export GOBIN="$sloctl_test_dir/gobin"
    export XDG_CACHE_HOME="$sloctl_test_dir/cache"
    cache_dir="$XDG_CACHE_HOME/nobl9/sloctl"
    release_url="http://127.0.0.1:38080/repos/nobl9/sloctl/releases/latest"
    make install VERSION=v1.0.0 NOTIFICATIONS_RELEASE_URL="$release_url"
    installed="$GOBIN/sloctl"
    mkdir -p "$sloctl_test_dir/manual"
    cp "$installed" "$sloctl_test_dir/manual/sloctl"
    reset_notice() {
      rm -f "$cache_dir/notifications.json" \
        "$cache_dir/skip-v1.1.0" "$cache_dir/skip-v1.2.0"
    }

    This keeps the test binary and notification cache separate from your normal installation. Keep this terminal open for the checks below.

Manual checks

Run these commands in terminal 2.

  1. Go installation and default Skip:

    reset_notice
    "$installed" version
    printf 'exit status: %s\n' "$?"

    Expect the release highlights, release link, and three choices. Update names go install github.com/nobl9/sloctl/cmd/sloctl@latest. Press Enter for Skip. Expect version output and exit status 0. Repeat on light and dark terminal backgrounds. All choices must remain readable.

  2. Unrecognized installation:

    reset_notice
    "$sloctl_test_dir/manual/sloctl" version
    printf 'exit status: %s\n' "$?"

    Expect identical highlights and a release link, with no choices or input required. Expect version output and exit status 0.

  3. Failed update recovery:

    reset_notice
    GOPROXY=http://127.0.0.1:1 GONOPROXY=none "$installed" version
    printf 'exit status: %s\n' "$?"

    Keep port 1 unused. Select Update. Expect a connection error, the original version output, and exit status 0.

  4. Run reset_notice before each additional check, except where the table says to retain state:

    Check Action and expected result
    Shell startup Run eval "$("$installed" completion bash)", then complete -p sloctl. Expect a registered completion function without a notice or prompt.
    Explicit Skip Run "$installed" version and select Skip. Expect version output and exit 0.
    Cache After Skip, rerun without reset_notice. Expect only the version.
    Skip until next version Select this choice. Remove only "$cache_dir/notifications.json" and rerun. The same release stays hidden. In terminal 1, stop the server and restart it with RELEASE_SERVER_TAG=v1.2.0 RELEASE_SERVER_HTML_URL=https://github.com/nobl9/sloctl/releases/tag/v1.2.0 python3 test/inputs/notifications/release_server.py "$(mktemp)" 38080. Remove only the timestamp file and rerun. Expect the new notice.
    Accessible mode Run SLOCTL_ACCESSIBLE_MODE=1 "$installed" version. Expect numbered choices with Skip as the default. Enter or bare Ctrl+D skips, prints the version, and exits 0. On a fresh run, enter x, then 2. Expect a validation message, another prompt, then version output and exit 0.
    Cancellation Run the normal form and press Ctrl+C. Expect exit 130 without version output.
    Fetch failure Stop the server in terminal 1. Reset the notice and run "$installed" version. Expect the version and exit 0.
    Malformed response In terminal 1, start RELEASE_SERVER_RAW_RESPONSE='{' python3 test/inputs/notifications/release_server.py "$(mktemp)" 38080. Reset and run sloctl. Expect the version and exit 0. Restore the normal server afterward.
    Opt-out Run SLOCTL_NO_NOTIFICATIONS=1 "$installed" version, then CI=1 "$installed" version. Expect only the version in both cases.
    Successful update, optional With the normal fixture and Go settings, select Update. Success exits 0 without running the version command. Restore the PR binary with the make install command above before further checks.

For Homebrew, use the optional Homebrew check before cleanup.

Cleanup: stop the server with Ctrl+C in terminal 1. In terminal 2, run rm -rf "$sloctl_test_dir", then close that test shell.

Manual testing: Windows with Dockur

Manual setup

  1. On the Linux host, install Podman and meet the Dockur Windows requirements. Start the VM:

    windows_vm_dir="$(mktemp -d)"
    mkdir -p "$windows_vm_dir/storage"
    podman run --detach --name sloctl-windows-review \
      --device=/dev/kvm --device=/dev/net/tun --cap-add NET_ADMIN \
      --publish 127.0.0.1:18006:8006 --stop-timeout 120 \
      --env VERSION=11 --env RAM_SIZE=6G --env CPU_CORES=4 \
      --env DISK_SIZE=64G \
      --volume "$windows_vm_dir/storage:/storage" \
      docker.io/dockurr/windows:latest
  2. Open http://127.0.0.1:18006 on the host. Let Windows install and complete setup with a local test account.

  3. Inside Windows, install Go 1.26.6 or newer and MSYS2. In MSYS2, run pacman -S --needed git make python. Check out this PR inside the guest.

  4. Open two MSYS2 terminals from guest PowerShell using the command below. Run it twice. This selects mintty with ConPTY disabled and preserves the Windows PATH:

    $env:MSYSTEM = 'UCRT64'
    $env:MSYS2_PATH_TYPE = 'inherit'
    & C:\msys64\usr\bin\mintty.exe --pcon off /usr/bin/bash --login
  5. In both MSYS2 terminals, enter the repository root. Confirm that go env GOOS prints windows.

  6. In guest terminal 1, start the local release server:

    export RELEASE_SERVER_BODY_FILE="$PWD/test/inputs/notifications/release-bodies/features-with-details.md"
    python3 test/inputs/notifications/release_server.py "$(mktemp)" 38080
  7. In guest terminal 2, build the Windows executable:

    unset CI SLOCTL_NO_NOTIFICATIONS SLOCTL_ACCESSIBLE_MODE
    sloctl_test_dir="$(mktemp -d)"
    GOBIN="$(cygpath -w "$sloctl_test_dir")"
    export GOBIN
    cache_dir="$(cygpath -u "$LOCALAPPDATA")/nobl9/sloctl"
    release_url="http://127.0.0.1:38080/repos/nobl9/sloctl/releases/latest"
    make install VERSION=v1.0.0 NOTIFICATIONS_RELEASE_URL="$release_url"
    installed="$sloctl_test_dir/sloctl.exe"
    reset_notice() {
      rm -f "$cache_dir/notifications.json" \
        "$cache_dir/skip-v1.1.0" "$cache_dir/skip-v1.2.0"
    }

Manual checks

  1. In guest terminal 2, run:

    reset_notice
    "$installed" version
    printf 'exit status: %s\n' "$?"

    Expect the highlights, release link, version output, and exit status 0. Windows must finish without choices or input, including for this Go installation.

  2. Reset and repeat with SLOCTL_ACCESSIBLE_MODE=1 "$installed" version. Expect the same behavior.

  3. Launch another mintty window with --pcon on instead of --pcon off. Repeat guest terminal 2 setup and check 1 in that window.

  4. Run cygpath -w "$installed" to get the executable's full Windows path. In PowerShell, run & '<full Windows path>' version. In Command Prompt, run "<full Windows path>" version. Replace the placeholder with the printed path. Reset the notice from MSYS2 before each run. Expect the same notice and exit 0 without input.

  5. Stop the server, reset, and rerun. Expect only the version and exit 0. Repeat with RELEASE_SERVER_RAW_RESPONSE='{' python3 test/inputs/notifications/release_server.py "$(mktemp)" 38080 in guest terminal 1.

  6. Restore the normal server. After separate resets, run SLOCTL_NO_NOTIFICATIONS=1 "$installed" version and CI=1 "$installed" version. Expect only the version.

Cleanup: stop the guest fixture, run reset_notice, and close the guest shells. On the host, run podman stop sloctl-windows-review and podman rm sloctl-windows-review. The disk remains in $windows_vm_dir/storage.

Manual testing: macOS with Dockur

Manual setup

  1. On the Linux host, install Podman and meet the Dockur macOS requirements. Start the VM:

    macos_vm_dir="$(mktemp -d)"
    mkdir -p "$macos_vm_dir/storage"
    podman run --detach --name sloctl-macos-review \
      --device=/dev/kvm --device=/dev/net/tun --cap-add NET_ADMIN \
      --publish 127.0.0.1:18007:8006 --stop-timeout 120 \
      --env VERSION=15 --env RAM_SIZE=6G --env CPU_CORES=4 \
      --env DISK_SIZE=64G \
      --volume "$macos_vm_dir/storage:/storage" \
      docker.io/dockurr/macos:latest

    These VM settings were exercised on Intel hardware. On AMD hosts, change CPU_CORES=4 to CPU_CORES=1 before starting the container.

  2. Open http://127.0.0.1:18007 on the host. Use Disk Utility to format the new 64 GiB virtual disk as APFS. Select Reinstall macOS and complete setup with a local test account.

  3. Inside macOS, install Go 1.26.6 or newer, Git, GNU Make, and Python 3. Check out this PR inside the guest.

  4. Open two Terminal windows in the repository root. Run bash in each window.

  5. In guest terminal 1, start the local release server:

    export RELEASE_SERVER_BODY_FILE="$PWD/test/inputs/notifications/release-bodies/features-with-details.md"
    python3 test/inputs/notifications/release_server.py "$(mktemp)" 38080
  6. In guest terminal 2, install the PR binary into a temporary Go installation directory:

    unset CI SLOCTL_NO_NOTIFICATIONS SLOCTL_ACCESSIBLE_MODE
    sloctl_test_dir="$(mktemp -d)"
    export GOBIN="$sloctl_test_dir/gobin"
    cache_dir="$HOME/Library/Caches/nobl9/sloctl"
    release_url="http://127.0.0.1:38080/repos/nobl9/sloctl/releases/latest"
    make install VERSION=v1.0.0 NOTIFICATIONS_RELEASE_URL="$release_url"
    installed="$GOBIN/sloctl"
    mkdir -p "$sloctl_test_dir/manual"
    cp "$installed" "$sloctl_test_dir/manual/sloctl"
    reset_notice() {
      rm -f "$cache_dir/notifications.json" \
        "$cache_dir/skip-v1.1.0" "$cache_dir/skip-v1.2.0"
    }

Manual checks

Run these commands in guest terminal 2.

  1. Go installation and default Skip:

    reset_notice
    "$installed" version
    printf 'exit status: %s\n' "$?"

    Expect the highlights, release link, and three choices. Press Enter for Skip. Expect version output and exit status 0.

  2. Unrecognized installation:

    reset_notice
    "$sloctl_test_dir/manual/sloctl" version
    printf 'exit status: %s\n' "$?"

    Expect identical highlights, with no choices or input required, then version output and exit status 0.

  3. Failed update recovery:

    reset_notice
    GOPROXY=http://127.0.0.1:1 GONOPROXY=none "$installed" version
    printf 'exit status: %s\n' "$?"

    Keep port 1 unused. Select Update. Expect a connection error, the original version output, and exit status 0.

  4. Run reset_notice before each additional check, except where the table says to retain state:

    Check Action and expected result
    Shell startup Run eval "$("$installed" completion bash)", then complete -p sloctl. Expect a registered completion function without a notice or prompt.
    Explicit Skip Run "$installed" version and select Skip. Expect version output and exit 0.
    Cache After Skip, rerun without reset_notice. Expect only the version.
    Skip until next version Select this choice. Remove only "$cache_dir/notifications.json" and rerun. The same release stays hidden. In guest terminal 1, stop the server and restart it with RELEASE_SERVER_TAG=v1.2.0 RELEASE_SERVER_HTML_URL=https://github.com/nobl9/sloctl/releases/tag/v1.2.0 python3 test/inputs/notifications/release_server.py "$(mktemp)" 38080. Remove only the timestamp file and rerun. Expect the new notice.
    Accessible mode Run SLOCTL_ACCESSIBLE_MODE=1 "$installed" version. Expect numbered choices with Skip as the default. Enter or bare Ctrl+D skips, prints the version, and exits 0. On a fresh run, enter x, then 2. Expect a validation message, another prompt, then version output and exit 0.
    Cancellation Run the normal form and press Ctrl+C. Expect exit 130 without version output.
    Fetch failure Stop the server in guest terminal 1. Reset and run "$installed" version. Expect the version and exit 0.
    Malformed response In guest terminal 1, start RELEASE_SERVER_RAW_RESPONSE='{' python3 test/inputs/notifications/release_server.py "$(mktemp)" 38080. Reset and run sloctl. Expect the version and exit 0. Restore the normal server afterward.
    Opt-out Run SLOCTL_NO_NOTIFICATIONS=1 "$installed" version, then CI=1 "$installed" version. Expect only the version in both cases.
    Successful update, optional With the normal fixture and Go settings, select Update. Success exits 0 without running the version command. Restore the PR binary with the make install command above before further checks.

For Homebrew, use the optional Homebrew check before cleanup.

Cleanup: stop the guest fixture, run reset_notice, and close the guest shells. On the host, run podman stop sloctl-macos-review and podman rm sloctl-macos-review. The disk remains in $macos_vm_dir/storage.

Homebrew check on Linux or macOS

The real Homebrew upgrade remains unrun locally. The official installer exited 1 in the Intel macOS guest with Homebrew on macOS is only supported on Apple Silicon processors!. The automated suite checks Homebrew command selection and failure recovery with a stub.

Run this optional check in a disposable Homebrew installation before platform cleanup. Install sloctl as described in the README. Use the PR binary and shell variables from that platform's setup:

brew_binary="$(brew --prefix --installed sloctl)/bin/sloctl"
cp "$installed" "$brew_binary"
reset_notice
"$brew_binary" version

Expect the same notice and choices, with brew upgrade sloctl as the update command. Check Skip, then reset and select Update. On success, expect exit 0 without the original version output. Restore the packaged binary with brew reinstall sloctl.

Release Notes

Added release notices with inline highlights and update choices for recognized Homebrew and Go installations on Linux and macOS. Windows displays release information without an update prompt. Fetch, cache, and updater failures let the requested command continue.

Show a cached TTY notification after successful commands when a newer sloctl release includes feature updates.
Add coverage for notifier behavior and CLI execution flow, including end-to-end notification tests.
nieomylnieja and others added 22 commits May 26, 2026 13:49
Detect how sloctl was installed so notifications can suggest the right upgrade
command for Homebrew, `go install`, or the install script fallback.
Show notifications for releases without feature entries and include bug-fix sections when extracting release notes.
Refresh the plain-text styling and wrap long install commands to fit narrower terminals.
Run update notifications before command execution and let users upgrade, skip,
or defer until the next version. Replace the GitHub proxy tests with
deterministic TTY fixtures and a local release fixture server.
Detect MinGW and Cygwin before showing the interactive update form.
Fall back to notification-only behavior for unsupported Windows shells.
Recognize Cygwin and MinGW terminals while suppressing the update form in
native Windows consoles. Add platform Bats coverage for Linux, macOS
Homebrew installs, and Windows.
Use semantic version ordering, preserve prompt interruption, and execute
detected updater commands safely. Expand release-note and CLI regression
coverage, and align test documentation and CI configuration.
@nieomylnieja nieomylnieja changed the title feat: add release feature notifications feat: add release notifications and interactive updates Jul 27, 2026
Detect Homebrew and Go installations reliably, persist skip preferences safely, and add installation guidance for unsupported setups. Extend notification tests and PTY helpers for update flows and timeout handling.
Comment thread test/setup_platform_suite.bash
Comment thread internal/style/theme.go Outdated
Comment thread internal/notifications/notifications.go Outdated
Comment thread internal/notifications/prompt.go Outdated
Comment thread internal/notifications/notifications_test.go Outdated
Comment thread internal/notifications/install.go Outdated
Comment thread internal/notifications/notifications.go
Comment thread internal/notifications/release_notes.go
@nieomylnieja
nieomylnieja added this pull request to stack #555 September 22, 2026 21:04
@annatrifonova annatrifonova added the passed-testing Added by QA when tests have passed label Sep 24, 2026
@nieomylnieja
nieomylnieja merged commit 9e7ee6c into main Sep 24, 2026
7 checks passed
@nieomylnieja
nieomylnieja deleted the add-new-feature-and-new-version-notifications branch September 24, 2026 10:37
nieomylnieja added a commit that referenced this pull request Sep 24, 2026
)

## Motivation

`make test/bats/platform` fails on macOS before any test runs:

```text
Could not find library 'bats-support' relative to test file or in BATS_LIB_PATH
```

The target runs Bats natively, and CI passes only because `bats-action`
installs Bats and its libraries and exports `BATS_LIB_PATH`.
Nothing documents that setup for local runs.
`docs/DEVELOPMENT.md` also does not list the other tools the Makefile
expects,
like Node.js, Docker or jq.

## Summary

### Platform tests in Docker

| Target | Before | After |
|---|---|---|
| `test/bats/platform` | Native, needs Bats on the host | Docker,
`platform:unix` tests in the Bats unit image |
| `test/bats/platform-native` | - | Native, the previous
`test/bats/platform` recipe |
| CI notification jobs | `make test/bats/platform` | `make
test/bats/platform-native` |
| `test/bats/unit` | Excludes platform tests | Unchanged |

- `test/bats/platform` works like the other Bats targets, so local runs
need
  no Bats on the host.
The `platform:macos` tests are also tagged `platform:unix`, so they run
  in the container too.
  The two `platform:windows` tests run only natively on Windows.
- CI keeps the native runs, because the macOS and Windows runners cannot
run
  the Linux image.
- The tests chose the sloctl binary by tag, so platform tests expected
the
  host binary from `make build`, which the image does not have.
  They now check for a container instead, the same check `setup_file`
  already used.
  Native runs keep using the host binary.

### Docs

- New prerequisites list in `docs/DEVELOPMENT.md`: Go, git, Node.js and
  Yarn 1.x, Docker and jq, each mapped to the targets that need it.
- New "Platform compatibility tests" section: `test/bats/platform` runs
in
  Docker, and `test/bats/platform-native` is the native CI run.
- Documents that the e2e tests read missing `SLOCTL_*` credentials from
the
  current sloctl context.
- Clarifies that `yarn` dependencies are also installed automatically,
  into `node_modules`.

### Install script tests

`install-script.bats` expected `linux amd64` in the installed binary's
version output.
On arm64 hosts, like Apple Silicon Macs, the container installs the
`linux-arm64` release and both install tests failed.
The tests now derive the expected architecture from `uname -m`.

### `make check` on macOS

- `unparam` failed with `resolveEditor - goOS always receives "darwin"`
because `runtime.GOOS` is a constant and the tests only passed the host
OS
  or `"darwin"`.
  CI lints on Linux, so it did not catch this.
  `TestResolveEditor` now also checks the Windows default editor.
- Replaced the deprecated gofumpt `extra-rules` setting with
  `extra.group-params`, which removes the warning from every lint run.

## Related changes

- Stacked on #468, which adds the native platform tests.
- #545 on `main` makes the Go docker e2e test read credentials from the
  current sloctl context, so this PR does not change that test.

## Testing

| Scenario | Result |
|---|---|
| `make test/bats/platform` on an arm64 Mac | 16 tests pass in Docker |
| `make test/bats/unit` on an arm64 Mac | 82 tests pass, including
install script tests |
| `BATS_LIB_PATH=<libs> make test/bats/platform-native` with bats on
`PATH`, as in CI | 16 tests pass with the host binary |
| `make check` on macOS | Pass, no lint issues or warnings |

`unit-tests.yml` runs only for pull requests into `main`, so the renamed
CI
target first runs on #468 once this PR is merged into it.

---------

Co-authored-by: Mateusz Hawrus <mateusz.hawrus@nobl9.com>
Co-authored-by: Mateusz Hawrus <48822818+nieomylnieja@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request go minor passed-testing Added by QA when tests have passed python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants