Skip to content

Fix self-upgrade interrupt handling - #425

Merged
kindermax merged 2 commits into
masterfrom
agent-add-self-upgrade-pre
Jul 29, 2026
Merged

Fix self-upgrade interrupt handling#425
kindermax merged 2 commits into
masterfrom
agent-add-self-upgrade-pre

Conversation

@kindermax

@kindermax kindermax commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fix lets self upgrade interrupt handling so an in-progress download responds to Ctrl-C by canceling the shared CLI context and then restoring normal signal behavior for any later interrupt.

Root Cause

The CLI signal handler kept SIGINT/SIGTERM registered after the first signal. If the self-upgrade path was still blocked while cancellation propagated, later Ctrl-C presses were still intercepted instead of falling back to the process default behavior.

Changes

  • Extract signal-to-context wiring into a small helper.
  • Stop signal notification after the first handled signal or parent context cancellation.
  • Add regression coverage for signal cancellation and signal notification cleanup.
  • Add an Unreleased changelog entry.

Validation

  • go test ./internal/cli -run TestSignalContext -count=1
  • go test ./...

Summary by Sourcery

Improve CLI signal handling to ensure self-upgrade downloads respond correctly to interrupts and restore default behavior after the first signal.

Bug Fixes:

  • Ensure lets self upgrade downloads are canceled on the first Ctrl-C and stop custom signal handling afterward.

Enhancements:

  • Extract reusable helper to wire OS signals into cancellable contexts for CLI operations.

Documentation:

  • Document the fix for lets self upgrade interrupt handling in the Unreleased changelog.

Tests:

  • Add regression tests covering signal-driven context cancellation and cleanup of signal notifications.

@sourcery-ai

sourcery-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors CLI signal handling into a reusable helper that wires OS signal notifications into a cancelable context, ensures signal notifications are stopped after the first handled interrupt or parent cancellation, and adds regression tests plus a changelog entry documenting the fix for Ctrl-C behavior during self-upgrade.

Sequence diagram for updated CLI signalContext handling

sequenceDiagram
    participant Main
    participant getContext
    participant signalContext
    participant signalNotify as signal.Notify
    participant signalStop as signal.Stop
    participant Goroutine
    participant ParentCtx as parent.Done
    participant Ch as chan_os_Signal

    Main->>getContext: getContext()
    getContext->>signalNotify: signal.Notify(Ch, os.Interrupt, SIGTERM)
    getContext->>signalContext: signalContext(context.Background(), Ch, stop)
    signalContext->>signalContext: context.WithCancel(parent)
    signalContext-->>getContext: ctx
    signalContext->>Goroutine: start goroutine

    loop wait_for_signal_or_parent_cancel
        alt OS sends SIGINT or SIGTERM
            OS-->>Ch: os.Signal
            Ch-->>Goroutine: os.Signal
            Goroutine->>Goroutine: log.Printf(signal received)
            Goroutine->>signalContext: cancel()
            Goroutine->>signalStop: stop()
        else parent context canceled
            ParentCtx-->>Goroutine: parent.Done()
            Goroutine->>signalContext: cancel()
            Goroutine->>signalStop: stop()
        end
    end

    Main-->>ctxUser: ctx.Done() (cancellation propagated)
Loading

File-Level Changes

Change Details Files
Refactor CLI context creation to use a dedicated signal-aware helper that stops signal notifications after the first handled signal or parent cancellation.
  • Replace inline signal.Notify wiring in getContext with a call to a new signalContext helper.
  • Implement signalContext to derive a cancelable context from a parent, listening for os.Interrupt and SIGTERM via a provided channel.
  • Ensure signalContext stops signal notification via a provided stop callback and cancels on either the first signal or parent context completion.
  • Log the received signal before canceling to preserve observability.
internal/cli/cli.go
Add regression tests for signal-driven context cancellation and signal notification cleanup.
  • Add TestSignalContext with a subtest that verifies the context is canceled and the stop callback is invoked after the first signal.
  • Add a second subtest that verifies the context cancels when the parent context is canceled, even without any signals.
  • Use timeouts in tests to fail fast if cancellation does not occur as expected.
internal/cli/cli_test.go
Document the fix for self-upgrade interrupt handling in the changelog.
  • Add an Unreleased changelog entry noting that lets self upgrade downloads now respond correctly to Ctrl-C.
docs/docs/changelog.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@kindermax
kindermax marked this pull request as ready for review July 29, 2026 12:30

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="internal/cli/cli_test.go" line_range="97-86" />
<code_context>
+		}
+	})
+
+	t.Run("cancels when parent context is canceled", func(t *testing.T) {
+		parent, cancel := context.WithCancel(context.Background())
+		ctx := signalContext(parent, make(chan os.Signal), func() {})
+
+		cancel()
+
+		select {
+		case <-ctx.Done():
+		case <-time.After(time.Second):
+			t.Fatal("expected signal context to be canceled by parent")
+		}
</code_context>
<issue_to_address>
**suggestion (testing):** Add an assertion that signal notification is stopped when the parent context is canceled

This subtest only checks that the child context is canceled when the parent is canceled; it doesn’t verify that the `stop` callback is invoked in this path. Since `signalContext` always defers `stop()`, consider mirroring the first subtest by passing a `stopped` channel and asserting it is closed when the parent is canceled, so the signal notification cleanup is covered for both signal- and parent-driven cancellation.
</issue_to_address>

### Comment 2
<location path="internal/cli/cli_test.go" line_range="74-72" />
<code_context>
 	}
 }

+func TestSignalContext(t *testing.T) {
+	t.Run("cancels and stops signal notification after first signal", func(t *testing.T) {
+		signals := make(chan os.Signal, 1)
+		stopped := make(chan struct{})
+		ctx := signalContext(context.Background(), signals, func() {
+			close(stopped)
+		})
+
+		signals <- os.Interrupt
+
+		select {
+		case <-ctx.Done():
+		case <-time.After(time.Second):
+			t.Fatal("expected signal context to be canceled")
+		}
+
+		select {
+		case <-stopped:
+		case <-time.After(time.Second):
+			t.Fatal("expected signal notification to stop")
+		}
+	})
+
+	t.Run("cancels when parent context is canceled", func(t *testing.T) {
+		parent, cancel := context.WithCancel(context.Background())
+		ctx := signalContext(parent, make(chan os.Signal), func() {})
+
+		cancel()
+
+		select {
+		case <-ctx.Done():
+		case <-time.After(time.Second):
+			t.Fatal("expected signal context to be canceled by parent")
+		}
+	})
+}
+
 func TestShouldCheckForUpdate(t *testing.T) {
</code_context>
<issue_to_address>
**suggestion (testing):** Consider a test case where the parent context is already canceled before creating the signal context

The existing subtests cover (1) cancellation on first signal and (2) cancellation when a live parent is later canceled. Please add a subtest for a parent context that is already canceled before `signalContext` is called, to verify the returned context is immediately done and the `stop` callback is invoked in that scenario.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread internal/cli/cli_test.go Outdated

select {
case <-ctx.Done():
case <-time.After(time.Second):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add an assertion that signal notification is stopped when the parent context is canceled

This subtest only checks that the child context is canceled when the parent is canceled; it doesn’t verify that the stop callback is invoked in this path. Since signalContext always defers stop(), consider mirroring the first subtest by passing a stopped channel and asserting it is closed when the parent is canceled, so the signal notification cleanup is covered for both signal- and parent-driven cancellation.

Comment thread internal/cli/cli_test.go
@@ -68,6 +71,43 @@ func TestFailOnConfigError(t *testing.T) {
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider a test case where the parent context is already canceled before creating the signal context

The existing subtests cover (1) cancellation on first signal and (2) cancellation when a live parent is later canceled. Please add a subtest for a parent context that is already canceled before signalContext is called, to verify the returned context is immediately done and the stop callback is invoked in that scenario.

@kindermax
kindermax merged commit 2a7303c into master Jul 29, 2026
5 checks passed
@kindermax
kindermax deleted the agent-add-self-upgrade-pre branch July 29, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant