Skip to content

fix(cli): propagate signal cancellation, expose post-push warnings in JSON - #19

Merged
benny123tw merged 1 commit into
mainfrom
fix/cli-cancellation-and-json-warnings
Apr 26, 2026
Merged

fix(cli): propagate signal cancellation, expose post-push warnings in JSON#19
benny123tw merged 1 commit into
mainfrom
fix/cli-cancellation-and-json-warnings

Conversation

@benny123tw

@benny123tw benny123tw commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Summary

Three small correctness fixes from a codebase audit:

  • Signal cancellation now reaches git/hooks. cli.Execute registers SIGINT/SIGTERM via fang.WithNotifySignal, and runNonInteractive passes cmd.Context() to executor.Execute instead of context.Background(). The executor already threads ctx into PushTag (exec.CommandContext) and RunHooks, so Ctrl+C now actually unwinds an in-flight push.
  • --json now reports post-push hook warnings. JSONOutput was missing a post_push_warnings field and outputJSON silently dropped result.PostPushWarnings — automation users were blind to fail-open hook failures that the text and TUI paths already surface.
  • TUI hook context lifecycle. Consolidated the per-hook CancelFunc cleanup into clearHookCancel() and call it in HookCompleteMsg, so the final hook's context is released as soon as it completes instead of lingering until the next phase starts (or never, for the last phase).

Test plan

  • just check — all packages pass, golangci-lint clean
  • New internal/cli/output_test.go — covers populated and empty PostPushWarnings JSON cases (verifies omitempty)
  • Manual: run a slow pre-tag hook and Ctrl+C — verify hook exits and tag is not created
  • Manual: configure a failing post-push hook and run with --json — verify warning appears in output

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • JSON output now includes a new optional post_push_warnings field that displays any warnings generated during push operations.
  • Improvements

    • Enhanced handling of interrupt signals (Ctrl+C and SIGTERM) for more graceful command termination and improved error reporting.
    • Improved context propagation in non-interactive mode for better error handling.

@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@benny123tw has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 56 minutes and 42 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 56 minutes and 42 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d448bbf0-ab58-4438-9619-d55b09fe0470

📥 Commits

Reviewing files that changed from the base of the PR and between 9fbed8e and 7c44f78.

📒 Files selected for processing (3)
  • internal/cli/output_test.go
  • internal/cli/root.go
  • internal/tui/model.go
📝 Walkthrough

Walkthrough

New tests validate outputJSON handling of post-push warnings. The JSONOutput struct gains an optional post_push_warnings field. CLI context passing is updated to use cmd.Context(). TUI string formatting refactored from fmt.Sprintf+WriteString to fmt.Fprintf. Hook cancellation logic centralized in TUI model.

Changes

Cohort / File(s) Summary
CLI Testing & Output Handling
internal/cli/output_test.go, internal/cli/root.go
New tests for outputJSON behavior with post-push warnings. JSONOutput struct extended with optional post_push_warnings field. Context passing updated to use cmd.Context() instead of context.Background(). Signal handling refactored for os.Interrupt and syscall.SIGTERM.
TUI Refactoring
internal/tui/confirm.go, internal/tui/model.go
String formatting refactored from fmt.Sprintf+WriteString to fmt.Fprintf in RenderConfirmation, RenderSuccess, RenderError, and version selection UI. Hook lifecycle cancellation centralized via new clearHookCancel() helper method.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Warnings now whisper through JSON streams,
Hooks cancel cleanly—no more broken dreams!
Strings format swiftly with fprintf's grace,
Context flows rightly through each CLI place.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the two main objectives: propagating signal cancellation to git/hooks and exposing post-push warnings in JSON output.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cli-cancellation-and-json-warnings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

… JSON

- Wire SIGINT/SIGTERM cancellation through fang.WithNotifySignal so Ctrl+C
  during a non-interactive bump unwinds the in-flight git push and hooks
  (executor.Execute already plumbs ctx into PushTag and RunHooks).
- Add PostPushWarnings to JSONOutput; outputJSON was silently dropping
  result.PostPushWarnings, leaving --json users blind to fail-open hook
  failures that the text and TUI paths already surface.
- Consolidate the per-hook CancelFunc cleanup in the TUI into
  clearHookCancel(), and call it in HookCompleteMsg so the final hook's
  context is released promptly instead of lingering until the next phase
  starts (or never, for the last phase).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@benny123tw
benny123tw force-pushed the fix/cli-cancellation-and-json-warnings branch from 9fbed8e to 7c44f78 Compare April 26, 2026 09:21

@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 enhances signal handling and output consistency by integrating signal notifications into the root command and including post-push warnings in the JSON output. It also refactors TUI rendering to use fmt.Fprintf and introduces better hook lifecycle management. A review comment points out that TUI hooks still use context.Background(), suggesting they should instead use the command context to ensure system signals are correctly propagated and handled during TUI execution.

Comment thread internal/tui/model.go
Comment on lines +905 to +910
func (m *Model) clearHookCancel() {
if m.hookCancelFunc != nil {
m.hookCancelFunc()
m.hookCancelFunc = nil
}
}

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

The addition of clearHookCancel is a good improvement for managing hook lifecycles. However, the hooks are still being started with context.Background() in startNextHook (line 882). To fully achieve the PR's goal of propagating signal cancellation, the TUI should ideally receive and use the command context (from cobra.Command) so that signals handled by fang in the root command can also trigger cancellation of in-flight TUI hooks.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (3)
internal/cli/output_test.go (1)

15-58: Test isolation: package-level flagDryRun leaks into outputJSON.

outputJSON reads the package-level flagDryRun (and other flags exist at package scope). These tests don't reset it, so a future test that flips flagDryRun = true without restoring could cause cross-test contamination. The current assertions don't check dry_run, so it isn't a failure today, but a small t.Cleanup (or saving/restoring the value) would harden this.

♻️ Suggested guard
 func TestOutputJSON_IncludesPostPushWarnings(t *testing.T) {
+	prev := flagDryRun
+	t.Cleanup(func() { flagDryRun = prev })
+	flagDryRun = false
 	cmd := &cobra.Command{}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cli/output_test.go` around lines 15 - 58, The tests call outputJSON
which reads the package-level flagDryRun (and other package flags), so add
isolation: in each test (e.g., TestOutputJSON_IncludesPostPushWarnings and
TestOutputJSON_OmitsEmptyPostPushWarnings) save the current value of flagDryRun,
set flagDryRun to the desired value for the test (likely false), and register a
t.Cleanup to restore the original value; alternatively use explicit save/restore
around the assertion rather than relying on global state—this ensures outputJSON
is exercised with a known dry-run value and prevents cross-test leakage.
internal/tui/model.go (1)

1058-1070: Cancellation gap: TUI push still uses context.Background().

The PR description states "Ctrl+C unwinds an in-flight push" — that holds for the non-interactive CLI (executor → exec.CommandContext), but the TUI path here detaches the push from any cancellable context. Combined with handleCtrlC blocking quit during StateExecuting and hookCancelFunc being nil during the actual push, a TUI user cannot interrupt a slow/hung git push.

Consider threading a cancellable context (and storing its CancelFunc similarly to hookCancelFunc) so the push respects the same double-ctrl+c cancellation UX as hooks. Pre-existing, but in-theme with this PR.

Sketch
// In Model fields:
//   pushCancelFunc context.CancelFunc

func (m Model) doPushAndPostPush() tea.Msg {
    if m.config.DryRun {
        return PushCompleteMsg{}
    }
    ctx, cancel := context.WithCancel(context.Background())
    m.pushCancelFunc = cancel  // note: requires pointer receiver to persist
    defer cancel()

    if err := m.config.Repository.PushTag(ctx, m.newVersion, m.config.Remote); err != nil {
        return ErrorMsg{Err: fmt.Errorf("failed to push tag: %w", err)}
    }
    return PushCompleteMsg{}
}

(Persisting the cancel func through Model requires moving this onto a pointer receiver and routing it through a tea.Cmd that captures *Model — same pattern as startNextHook already uses.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/tui/model.go` around lines 1058 - 1070, The TUI push uses
context.Background() in doPushAndPostPush which prevents Ctrl+C cancellation;
change Model.doPushAndPostPush to a pointer receiver, create a cancellable
context via context.WithCancel, store the cancel function on a new Model field
(e.g. pushCancelFunc) alongside the existing hookCancelFunc, defer cancel
locally and use the cancellable ctx when calling m.config.Repository.PushTag,
and ensure the tea.Cmd that invokes doPushAndPostPush captures *Model (same
pattern as startNextHook) so the cancel func persists and Ctrl+C can cancel an
in-flight push.
internal/tui/confirm.go (1)

67-67: Refactor missed two sb.WriteString(fmt.Sprintf(...)) sites.

The PR normalizes this pattern across confirm.go, but lines 67 and 99 still use the old form. Worth folding into the same cleanup for consistency.

♻️ Proposed diff
-	sb.WriteString(SuccessStyle.Render(fmt.Sprintf("%s Success!", IconCheck)))
+	fmt.Fprintf(&sb, "%s\n\n", SuccessStyle.Render(fmt.Sprintf("%s Success!", IconCheck)))

Or, more cleanly, keep WriteString but skip the inner Sprintf:

-	sb.WriteString(SuccessStyle.Render(fmt.Sprintf("%s Success!", IconCheck)))
+	sb.WriteString(SuccessStyle.Render(IconCheck + " Success!"))
...
-	sb.WriteString(ErrorStyle.Render(fmt.Sprintf("%s Error", IconCross)))
+	sb.WriteString(ErrorStyle.Render(IconCross + " Error"))

Also applies to: 99-99

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/tui/confirm.go` at line 67, Two spots still wrap a literal in
fmt.Sprintf inside SuccessStyle.Render when calling sb.WriteString; replace
sb.WriteString(SuccessStyle.Render(fmt.Sprintf("%s Success!", IconCheck))) (and
the similar occurrence around IconCheck on the other line) by removing the inner
fmt.Sprintf and passing a plain string (e.g.
sb.WriteString(SuccessStyle.Render(IconCheck+" Success!"))), keeping the same
sb.WriteString + SuccessStyle.Render pattern and updating both occurrences
referencing sb.WriteString, SuccessStyle.Render, and IconCheck.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/cli/output_test.go`:
- Around line 15-58: The tests call outputJSON which reads the package-level
flagDryRun (and other package flags), so add isolation: in each test (e.g.,
TestOutputJSON_IncludesPostPushWarnings and
TestOutputJSON_OmitsEmptyPostPushWarnings) save the current value of flagDryRun,
set flagDryRun to the desired value for the test (likely false), and register a
t.Cleanup to restore the original value; alternatively use explicit save/restore
around the assertion rather than relying on global state—this ensures outputJSON
is exercised with a known dry-run value and prevents cross-test leakage.

In `@internal/tui/confirm.go`:
- Line 67: Two spots still wrap a literal in fmt.Sprintf inside
SuccessStyle.Render when calling sb.WriteString; replace
sb.WriteString(SuccessStyle.Render(fmt.Sprintf("%s Success!", IconCheck))) (and
the similar occurrence around IconCheck on the other line) by removing the inner
fmt.Sprintf and passing a plain string (e.g.
sb.WriteString(SuccessStyle.Render(IconCheck+" Success!"))), keeping the same
sb.WriteString + SuccessStyle.Render pattern and updating both occurrences
referencing sb.WriteString, SuccessStyle.Render, and IconCheck.

In `@internal/tui/model.go`:
- Around line 1058-1070: The TUI push uses context.Background() in
doPushAndPostPush which prevents Ctrl+C cancellation; change
Model.doPushAndPostPush to a pointer receiver, create a cancellable context via
context.WithCancel, store the cancel function on a new Model field (e.g.
pushCancelFunc) alongside the existing hookCancelFunc, defer cancel locally and
use the cancellable ctx when calling m.config.Repository.PushTag, and ensure the
tea.Cmd that invokes doPushAndPostPush captures *Model (same pattern as
startNextHook) so the cancel func persists and Ctrl+C can cancel an in-flight
push.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f24e2c4d-fd50-43ec-9bda-f0c7314b2138

📥 Commits

Reviewing files that changed from the base of the PR and between 5ba71ea and 9fbed8e.

📒 Files selected for processing (4)
  • internal/cli/output_test.go
  • internal/cli/root.go
  • internal/tui/confirm.go
  • internal/tui/model.go

@benny123tw
benny123tw merged commit 659f2cd into main Apr 26, 2026
9 checks passed
@benny123tw
benny123tw deleted the fix/cli-cancellation-and-json-warnings branch April 26, 2026 09:23
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