Skip to content

fix(adopt): handle CTRL+C cleanly during av adopt (#692)#749

Open
mvanhorn wants to merge 3 commits into
aviator-co:masterfrom
mvanhorn:fix/692-av-adopt-ctrl-c-cancellation
Open

fix(adopt): handle CTRL+C cleanly during av adopt (#692)#749
mvanhorn wants to merge 3 commits into
aviator-co:masterfrom
mvanhorn:fix/692-av-adopt-ctrl-c-cancellation

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

Summary

av adopt now exits cleanly on SIGINT (CTRL+C). Threads context.Context through the adopt path, switches git subprocess invocations to exec.CommandContext, and adds a shared signal.NotifyContext install at the Cobra entry.

Why this matters

From #692:

Only way to stop it appears to be to get the process ID and run kill -9 on it.

Other av commands (av sync) honor SIGINT correctly, so the fix mirrors that pattern.

Changes

  • cmd/av/main.go: install signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) once at the Cobra root
  • cmd/av/adopt.go: propagate the context into the adopt action
  • internal/actions/adopt_branches.go, find_adoptable_local_branches.go, get_remote_stacked_pr.go, git_fetch.go: honor ctx.Done() in branch-walk loops
  • internal/treedetector/detector.go, internal/utils/uiutils/tea.go: thread context through
  • e2e_tests/adopt_test.go: new test spawning av adopt, sending SIGINT, asserting bounded exit time

Partial-state cleanup is correct: no half-adopted branches left in inconsistent state on cancellation.

Fixes #692

@mvanhorn mvanhorn requested a review from a team as a code owner May 16, 2026 19:53
@aviator-app

aviator-app Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Current Aviator status

Aviator will automatically update this comment as the status of the PR changes.
Comment /aviator refresh to force Aviator to re-examine your PR (or learn about other /aviator commands).

This pull request is currently open (not queued).

How to merge

To merge this PR, comment /aviator merge or add the mergequeue label.


See the real-time status of this PR on the Aviator webapp.
Use the Aviator Chrome Extension to see the status of your PR within GitHub.

@aviator-app

aviator-app Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

🔃 FlexReview Status

Common Owner: aviator-co/engineering (expert-load-balance assignment)
Owner and Assignment:

  • 🔒 aviator-co/engineering (expert-load-balance assignment)
    Owned Files
    • 🔒 e2e_tests/sigint_test.go
    • 🔒 cmd/av/adopt.go
    • 🔒 cmd/av/main.go
    • 🔒 internal/actions/adopt_branches.go
    • 🔒 internal/actions/find_adoptable_local_branches.go
    • 🔒 internal/actions/get_remote_stacked_pr.go
    • 🔒 internal/actions/git_fetch.go
    • 🔒 internal/treedetector/detector.go
    • 🔒 internal/utils/uiutils/tea.go

Review SLO: 7 business hours if PR size is <= 200 LOC for the first response.
❕ This PR modifies 233 lines, which is larger than the Review SLO threshold.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request implements context propagation and signal handling across the av command suite, enabling graceful termination on SIGINT and SIGTERM. Key changes include the introduction of RunBubbleTeaWithContext, context-aware Git operations, and a new E2E test verifying interrupt behavior. Review feedback identifies a potential data race in GetRemoteStackedPRModel and suggests refactoring the context storage in view models from provider functions to direct struct fields for better idiomatic Go compliance.

nextPRNumber := int64(0)
for {
if err := m.ctx().Err(); err != nil {
m.failed = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

This line introduces a data race. The field m.failed is being modified inside a goroutine (the anonymous function returned as a tea.Cmd), while it may be read concurrently by the main thread in the View method. In the Bubble Tea architecture, models should only be modified within the Update method to ensure thread safety. Consider returning the error as a message and handling it in Update to set the failure state safely.

}

type AdoptBranchesModel struct {
ctx func() context.Context

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

It is more idiomatic in Go to store the context.Context directly in the struct rather than using a provider function func() context.Context, especially since the context is passed during construction and remains stable. This simplifies the code by allowing direct access (e.g., m.ctx.Err()) and avoids unnecessary closure overhead. Please apply this change here and consistently across other view models in this PR.

Suggested change
ctx func() context.Context
ctx context.Context

onDone func() tea.Cmd,
) tea.Model {
return &AdoptBranchesModel{
ctx: func() context.Context { return ctx },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Following the suggestion to store the context directly, the initialization should be updated to assign the context value.

Suggested change
ctx: func() context.Context { return ctx },
ctx: ctx,

mvanhorn added 2 commits May 16, 2026 14:38
The Init() goroutine was mutating m.failed, m.prs, and m.done directly,
while View() reads those fields from the Bubble Tea main thread - a real
data race flagged by gemini-code-assist.

Replace the in-goroutine mutations with two new message types
(remoteStackedPRFailedMsg, remoteStackedPRDoneMsg) emitted by the
goroutine and handled in Update(). All field writes now happen on the
main thread, matching the Bubble Tea model contract.

Behavior is preserved: failure surfaces as colors.FailureStyle, the
onDone command still fires after a successful collection, and ctrl+c
cancellation still aborts via m.ctx().

The e2e_tests/adopt_test.go addition was dropped during rebase because
master migrated to the testscript framework (aviator-co#695); the SIGINT e2e
coverage needs to be reauthored as a testscript .txtar before re-adding.

Signed-off-by: Matt Van Horn <[email protected]>
@mvanhorn mvanhorn force-pushed the fix/692-av-adopt-ctrl-c-cancellation branch from ffd106a to eaee61d Compare May 16, 2026 21:40
@mvanhorn

Copy link
Copy Markdown
Contributor Author

Rebased onto master (master picked up the testscript e2e migration in #695) and pushed eaee61d to address the HIGH-priority data race.

State mutations on GetRemoteStackedPRModel (m.failed, m.prs, m.done) are now routed through Update() via two new message types (remoteStackedPRFailedMsg, remoteStackedPRDoneMsg). The Init() goroutine collects PRs into a local slice and only emits messages; all field writes happen on the Bubble Tea main thread, matching the framework contract. Build, go test ./internal/actions/..., and golangci-lint all clean.

The e2e_tests/adopt_test.go TestAdopt_ExitsOnSIGINT addition was dropped during rebase because master migrated to the testscript framework. I'll re-author it as a .txtar in a follow-up if you want explicit e2e SIGINT coverage; happy to fold that in here too if you'd prefer.

Not tackling the two medium suggestions (storing context.Context directly in adopt_branches.go) in this push so the data race fix lands clean. Will follow up.

Re-introduces TestAdopt_ExitsOnSIGINT (originally dropped during the
testscript rebase) as a plain Go test in e2e_tests/sigint_test.go.

The test spawns av adopt --dry-run with PATH-injected fake git that
sleeps on `merge-base`, waits for the marker file confirming the
sleep started, sends os.Interrupt, and asserts av returns within 5s.
This locks in the SIGINT path through cmd/av/main.go's
signal.NotifyContext + uiutils.RunBubbleTeaWithContext + the
adopt_branches / get_remote_stacked_pr ctx checks.

The test bypasses the testscript harness deliberately. testscript
v1.10.0 (the version currently in go.mod) sends os.Interrupt to all
backgrounded processes only at end-of-test via waitBackground, and
exposes no public API for sending a signal to a specific named
background process from a script. A header comment documents the
escape hatch and the conditions under which this can move into
testdata/script as a .txtar.

Follow-up to aviator-co#749's gemini-flagged feedback (SIGINT e2e coverage).

Signed-off-by: Matt Van Horn <[email protected]>
@mvanhorn

Copy link
Copy Markdown
Contributor Author

Follow-up on the two medium gemini items.

SIGINT e2e coverage: pushed ba7620b adding TestAdoptExitsOnSIGINT in e2e_tests/sigint_test.go. It re-uses the merge-base sleep trick the original test had: spawn av adopt --dry-run with PATH-injected fake git that sleeps on merge-base, wait for the marker confirming the sleep started, send os.Interrupt, and assert av returns within 5s. Locks in the SIGINT path through signal.NotifyContext -> RunBubbleTeaWithContext -> the per-model ctx checks. Test passes in ~0.7s. The file deliberately sits outside the testscript harness - testscript v1.10.0 in go.mod has no public API for sending an arbitrary signal to a specific named background process (it only sends os.Interrupt to every backgrounded command at end-of-test via waitBackground). A header comment in the file documents the conditions under which this can migrate into testdata/script as a .txtar.

context.Context direct storage on AdoptBranchesModel / GetRemoteStackedPRModel: tried the suggested refactor, both models then trip the containedctx golangci-lint rule that the repo already enforces. Reverted - keeping the func() context.Context closure indirection consistent with the rest of the codebase. Happy to revisit if the lint rule changes.

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.

Bug: When you CTRL+C when running av adopt, nothing happens

1 participant