Commit fca83f4
feat(provision): prompt to cancel Azure deployment on Ctrl+C (Bicep) (#7795)
* feat(provision): prompt to cancel Azure deployment on Ctrl+C (Bicep)
When a user presses Ctrl+C during 'azd provision' or 'azd up' while a
Bicep deployment is in flight on Azure, azd now pauses and asks whether
to leave the Azure deployment running (default) or to cancel it via the
ARM Cancel API and wait for a terminal state.
- pkg/input: register-able interrupt handler stack with re-entrant
Ctrl+C suppression while a handler is running.
- pkg/azapi + pkg/infra: Cancel methods on DeploymentService /
Deployment for both subscription- and resource-group-scoped
deployments. Deployment Stacks return 'not supported' (no Cancel API
surface today).
- pkg/infra/provisioning: typed sentinel errors for the 4 outcomes
(leave running / canceled / cancel timed out / cancel too late) plus
telemetry attribute provision.cancellation.
- pkg/infra/provisioning/bicep: interactive prompt + cancel-and-poll
flow with 30s cancel-request timeout and 2-min terminal-state wait.
- cmd/middleware + internal/cmd: bypass agent troubleshooting and map
sentinels to telemetry codes.
- docs/provision-cancellation.md: user-facing behavior, outcomes,
provider scope, telemetry, and non-interactive fallback.
Terraform and Deployment Stacks are out of scope and unchanged.
Closes #2810
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address Copilot review feedback (iteration 1)
- pkg/input: LIFO test now invokes handlers and asserts distinct call
counts to prove ordering.
- pkg/infra/provisioning: add ErrDeploymentCancelFailed sentinel so the
cancel-request-failure path no longer misclassifies as a timeout;
wire it through error middleware skip-list and telemetry mapping.
- pkg/infra: switch new TestScopeCancel subtests to t.Context().
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address Copilot review feedback (iteration 2)
- pkg/azapi: add typed ErrCancelNotSupported sentinel; stack
CancelSubscriptionDeployment / CancelResourceGroupDeployment now
return it instead of an opaque string.
- pkg/infra/provisioning/bicep: interrupt handler treats
ErrCancelNotSupported as the safer 'leave running' outcome (matches
documented stacks behavior + telemetry). Cancel-request error path
routes through terminalToOutcome when the deployment is already in a
terminal state, so the portal URL and consistent messaging are
surfaced. Canceled terminal branch now prints the portal URL too.
- pkg/infra/provisioning: ErrDeploymentCancelFailed doc comment now
references errors.Is/errors.As (matches the multi-%w joined-error
wrapping pattern used here).
- pkg/infra/provisioning/bicep/bicep_provider: tear down the interrupt
handler immediately after deployModule returns (sync.OnceFunc) to
avoid a small window where a late Ctrl+C could surface the prompt
over post-processing output.
- internal/cmd/errors: map ErrCancelNotSupported in classifySentinel.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: close interrupt-vs-natural-completion race (iteration 3)
If Ctrl+C arrives but the ARM deployment happens to finish naturally
before the user picks an option in the prompt, the previous design
could take the success path and silently drop the interrupt.
- installDeploymentInterruptHandler now exposes a 'started' channel
that is closed the instant Ctrl+C is received, before the prompt is
shown. deployCtx is also cancelled immediately so PollUntilDone
unblocks ASAP.
- BicepProvider.Deploy block-receives the outcome whenever 'started'
is closed (instead of a non-blocking drain), so the user's choice is
always honored regardless of who wins the race.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: re-entrant Ctrl+C suppression hardening (iteration 4)
- pkg/input/console: watchTerminalInterrupt now reserves the running
slot before consulting the handler stack so re-entrant Ctrl+C is
suppressed even if the stack is briefly empty (e.g. handler popped
but still executing the prompt).
- pkg/infra/provisioning/bicep/bicep_provider: defer cleanup until
after the interrupt outcome is received so a second Ctrl+C during
the prompt is still suppressed; the no-interrupt path tears down
immediately as before.
- pkg/infra/provisioning/cancel: doc reads 'sentinel errors' instead
of 'typed errors' to match the implementation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address Copilot review feedback (iteration 5)
- pkg/input/interrupt: enforce strict LIFO when popping handlers
(only pop when this handler is still top-of-stack), so out-of-order
pops never accidentally remove unrelated newer handlers.
- pkg/infra/provisioning/bicep/interrupt: defensive default in
terminalToOutcome now stops the spinner and emits a warning with
the observed state and portal URL, leaving the UI clean if an
unexpected terminal state is ever observed.
- pkg/infra/provisioning/bicep/interrupt: treat
DeploymentProvisioningStateDeleted as terminal in the cancel poll
so we don't keep polling until the deadline if the deployment is
deleted out from under us. Test updated accordingly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address Copilot review feedback (iteration 6)
- pkg/infra/provisioning/bicep/interrupt: wrap the interrupt handler
closure with sync.OnceValue so close(started), cancelDeploy() and
the outcome channel send all run at most once. Combined with the
in-flight guard from tryStartInterruptHandler and the strict LIFO
pop, additional Ctrl+C signals after the prompt completes can no
longer panic or block on the buffered channel.
- pkg/infra/provisioning/bicep/interrupt: print the portal URL on
the prompt-failure leave-running path so the user always has a
link to follow up when the URL is available.
- docs/provision-cancellation: clarify that the portal URL is
printed when available (not 'in every case').
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address Copilot review feedback (iteration 7)
- pkg/input/interrupt: nil out the popped slot before truncating
the interrupt stack so the GC can reclaim the popped handler and
any state it captured, even before the underlying array is
reallocated.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address Copilot review feedback (iteration 8)
- pkg/input/console: run the registered interrupt handler inline on
the signal goroutine instead of in a nested goroutine. This
removes the scheduling window where SIGINT was received but the
handler had not yet run, which could let a deploy goroutine
complete naturally and silently drop the Ctrl+C. Re-entrant signals
remain suppressed via tryStartInterruptHandler.
- pkg/infra/provisioning/bicep/interrupt: switch the cancel poll
loop to a time.Ticker and move the wait before each Get, so a
slow Get cannot produce back-to-back ARM polls (preventing
throttling).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address review findings from wbreza and jongio on PR #7795
Fixes from wbreza's review:
- H1/N1: Race between deploy-success and interrupt handler — replaced
select-based check with atomic CAS state machine (deployStateRunning →
deployStateInterrupting or deployStateCompleted) so the handler and
Deploy goroutine never conflict.
- H2: Panic in interrupt handler — added recover() with stack trace
logging in watchTerminalInterrupt so a handler panic doesn't leave the
process unkillable.
- H3: Second Ctrl+C force-exit — added forceExitPending counter in
interrupt.go; second suppressed Ctrl+C while a handler is running
triggers os.Exit(130) matching POSIX convention (kubectl, terraform).
- M13: terminalToOutcome now a BicepProvider method with ctx as first
parameter per AGENTS.md convention.
- L7: Spelling consistency — 'Cancelling' → 'Canceling' to match ARM
API and codebase convention.
- L8: Removed stray blank line in cmd/middleware/error.go.
Fixes from jongio's review:
- N1: Same race fix as H1 above (CAS state machine).
- N2: Panic recovery (defense-in-depth per Jon's suggestion).
- N3: Test cleanup — added t.Cleanup() for PushInterruptHandler pops
and finishInterruptHandler to prevent global state leaks on assertion
failure.
Fixes from Copilot bot review:
- Unbounded Get call in cancel-request error path — added
context.WithTimeout wrapper (30s).
- DeploymentUrl fetch in prompt — added timeout to prevent indefinite
blocking on slow/unreachable ARM.
- Deleted state mismatch — added explicit case in terminalToOutcome for
DeploymentProvisioningStateDeleted.
- Test cleanup in interrupt_test.go (same as N3 above).
New tests:
- TestForceExitCounter: validates force-exit on 2nd suppressed Ctrl+C.
- TestForceExitCounter_ResetsOnNewHandler: ensures counter resets
between handler lifecycles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(bicep): add unit tests for interrupt prompt/cancel/install flow
Addresses follow-up review feedback from @wbreza on PR #7795:
- Adds targeted unit tests for runInterruptPrompt,
cancelAndAwaitTerminal, and installDeploymentInterruptHandler
using a programmable fake infra.Deployment and the existing
MockConsole. Tests cover the leave-running, cancel,
prompt-failure, URL-fetch-failure, cancel-not-supported,
cancel-failed-with-fallback-Get, polled-canceled, and
poll-timeout paths, plus the markCompleted/interrupt CAS race.
- Promotes cancelRequestTimeout, cancelTerminalTimeout, and
cancelPollInterval from const to package-level var so unit
tests can shrink them to keep the suite sub-second.
- Logs the post-cancel Get error when the cancel API itself
failed and the fallback Get also fails (it was previously
silently dropped, hurting production diagnosability).
- Exposes input.SnapshotInterruptStack as a test-only helper so
cross-package tests can invoke the registered handler without
installing the OS signal pipeline.
Refs: #7933 (follow-up
for replacing the process-global interrupt state with an
injectable InterruptBroker).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* perf(bicep): issue first cancel-poll Get immediately
Previously cancelAndAwaitTerminal entered a ticker-driven loop that
waited cancelPollInterval (5s in production) BEFORE every Get,
including the first one issued right after the cancel API succeeded.
For deployments that Azure transitions to Canceled quickly (e.g.
deployments that just started), the user saw a needless ~5s pause
before azd reported the cancellation.
Fix: do an immediate Get right after the cancel request returns; only
the subsequent retries are ticker-spaced. This preserves the original
'no back-to-back Gets' guarantee for the slow path while removing the
unnecessary delay on the fast path.
Adds TestCancelAndAwaitTerminal_FirstGetIsImmediate which sets
cancelPollInterval to a deliberately large value and asserts the call
returns in well under a poll interval when the first Get already
returns Canceled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(bicep): wait for nested deployments after cancel + URL formatting
Addresses review comments on PR #7795:
- #5/#6: Wrap portal URLs in output.WithLinkFormat across all
user-facing emission sites; extract printLeaveRunningMessage helper
to deduplicate the leave-running prompt body.
- #7: After the top-level deployment reaches Canceled, walk the
operations tree to discover descendant (nested) deployments,
best-effort cancel any that are still non-terminal, and wait for
them to reach a terminal state. The whole interrupt flow now lives
under a single 5-minute global budget (previously 2-minute terminal
timeout). If one or more nested deployments remain non-terminal at
budget exhaustion, azd surfaces them by name with portal links and
records a new 'cancel_timed_out_nested' telemetry value (still
ErrDeploymentCancelTimeout).
- #9: Fix misleading 'second Ctrl+C is treated as a force-exit'
comment in console.go — the second additional press arms the
force-exit latch (and is suppressed); the next press triggers exit.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* ux(bicep): clearer interrupt copy + per-state telemetry split
Addresses customer-experience review on PR #7795 (kristenwomack):
- R1/R11 Branch terminalToOutcome by state. Succeeded is reframed as a
*success* ("completed successfully before cancellation took effect —
your resources are deployed") so users who Ctrl+C suspecting a hang
aren't told their successful deployment was a 'too-late' failure.
Failed/Deleted get distinct copy. Telemetry value split into
cancel_raced_succeeded / _failed / _deleted; cancel_too_late kept as
the fallback for unexpected terminal states.
- R2 Non-interactive prompt fallback now always emits a breadcrumb
(portal URL when available, otherwise 'find it under
Subscription → Deployments') plus the az-deployment-cancel hint, so
CI runs can't silently leak abandoned Azure deployments.
- R3 Prompt title is now active and names the cause: 'You pressed
Ctrl+C. An Azure deployment is still running — what do you want to
do?'.
- R4 Prompt help text and leave-running message no longer drop the
pointer when the URL fetch fails — they degrade to a portal-search
hint that includes the deployment name.
- R9 Replace the leaking Go duration in the cancel-timeout message
('within 5m0s') with prose ('within 5 minutes'). Title updated to
'Azure is still canceling — azd will exit'.
- R10 Cancel-failed copy reframed: 'Couldn't cancel — Azure deployment
is still running. The cancel request was rejected by Azure. The
deployment will continue.'
- R15 'Deployment was deleted' message now explains it's unusual and
suggests checking audit logs.
- R16 Leave-running path always prints the
'az deployment sub|group cancel --name <n>' hint so users have a
copy-pasteable next step.
Docs (provision-cancellation.md) and the ProvisionCancellationKey field
comment updated to describe the new telemetry values.
R5 (option ordering), R6 (5-min wait UX), R7 (discoverability),
R8 (docs site), R12 (skip prompt for Stacks), R13 (a11y),
R14 (color/WCAG) are tracked in a follow-up issue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>1 parent afb62d5 commit fca83f4
16 files changed
Lines changed: 2172 additions & 5 deletions
File tree
- cli/azd
- cmd/middleware
- docs
- internal
- cmd
- tracing/fields
- pkg
- azapi
- infra
- provisioning
- bicep
- input
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| 33 | + | |
33 | 34 | | |
34 | 35 | | |
35 | 36 | | |
| |||
86 | 87 | | |
87 | 88 | | |
88 | 89 | | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
89 | 95 | | |
90 | 96 | | |
91 | 97 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
295 | 295 | | |
296 | 296 | | |
297 | 297 | | |
| 298 | + | |
| 299 | + | |
298 | 300 | | |
299 | 301 | | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
300 | 312 | | |
301 | 313 | | |
302 | 314 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
454 | 454 | | |
455 | 455 | | |
456 | 456 | | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
457 | 481 | | |
458 | 482 | | |
459 | 483 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
35 | 40 | | |
36 | 41 | | |
37 | 42 | | |
| |||
226 | 231 | | |
227 | 232 | | |
228 | 233 | | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
229 | 253 | | |
230 | 254 | | |
231 | 255 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
677 | 677 | | |
678 | 678 | | |
679 | 679 | | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
680 | 703 | | |
681 | 704 | | |
682 | 705 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
580 | 580 | | |
581 | 581 | | |
582 | 582 | | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
583 | 624 | | |
584 | 625 | | |
585 | 626 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
875 | 875 | | |
876 | 876 | | |
877 | 877 | | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
878 | 883 | | |
879 | | - | |
| 884 | + | |
880 | 885 | | |
881 | 886 | | |
882 | 887 | | |
883 | 888 | | |
884 | 889 | | |
885 | 890 | | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
886 | 911 | | |
| 912 | + | |
887 | 913 | | |
888 | 914 | | |
889 | 915 | | |
| 916 | + | |
| 917 | + | |
890 | 918 | | |
891 | 919 | | |
892 | 920 | | |
| |||
0 commit comments