Standardize no-prompt required input behavior #7825
Conversation
There was a problem hiding this comment.
Pull request overview
This PR aims to standardize how azd behaves when --no-prompt is enabled by introducing a shared input.PromptRequiredError and updating multiple prompt paths to return consistent, structured “prompt required” failures (human-readable + JSON) instead of ad hoc strings/defaulting.
Changes:
- Added
input.PromptRequiredError(string + JSON) and a sharedcontracts.ErrorEnvelope. - Updated no-prompt behavior in the input asker, gRPC prompt service, provisioning manager, and Azure context helpers to use the shared error.
- Adjusted prompt-service tests to validate the new standardized error contract.
Show a summary per file
| File | Description |
|---|---|
| cli/azd/pkg/prompt/prompter.go | Extends prompt.Prompter with IsNoPromptMode() for consistent no-prompt checks. |
| cli/azd/pkg/prompt/prompt_service.go | Removes ad hoc no-prompt auto-selection logic and introduces PromptRequiredError usage in some prompt paths. |
| cli/azd/pkg/prompt/prompt_service_test.go | Reworks tests to expect PromptRequiredError with structured missing-input details. |
| cli/azd/pkg/prompt/prompt_models.go | Adds GlobalCommandOptions awareness to AzureContext and returns PromptRequiredError for missing scope fields in no-prompt. |
| cli/azd/pkg/prompt/azure_context_test.go | Updates NewAzureContext constructor calls for the new signature. |
| cli/azd/pkg/infra/provisioning/manager.go | Returns PromptRequiredError for missing subscription/location when no-prompt mode is active. |
| cli/azd/pkg/infra/provisioning/manager_test.go | Adds coverage validating the new no-prompt missing-input errors. |
| cli/azd/pkg/input/prompt_required_error.go | Introduces the shared structured error type and formatting/JSON contract. |
| cli/azd/pkg/input/prompt_required_error_test.go | Adds unit tests for PromptRequiredError output and JSON. |
| cli/azd/pkg/input/asker.go | Converts no-default no-prompt failures into PromptRequiredError (prompt-message variant). |
| cli/azd/pkg/input/asker_test.go | Updates tests to assert typed PromptRequiredError behavior. |
| cli/azd/pkg/contracts/envelope.go | Adds a reusable ErrorEnvelope type used by PromptRequiredError JSON. |
| cli/azd/internal/grpcserver/prompt_service.go | Returns PromptRequiredError for extension prompt RPCs in no-prompt mode. |
| cli/azd/pkg/pipeline/pipeline_manager_test.go | Updates mock prompter to satisfy new interface. |
| cli/azd/cmd/deeper_coverage3_test.go | Updates mock prompter to satisfy new interface. |
Copilot's findings
Comments suppressed due to low confidence (2)
cli/azd/pkg/prompt/prompt_service.go:323
PromptLocationcurrently computes a default location (fallbackeastus2/ user config) and relies on the selector’s default index. In--no-promptmode this can silently select a location (or callGetLocationsand then fail with a prompt-message-only error) instead of emitting the standardized missing-input error. Add an explicitNoPromptcheck to returninput.PromptRequiredErrorwith required input sources (e.g.,AZURE_LOCATION) before loading locations when no location was provided non-interactively.
// PromptLocation prompts the user to select an Azure location.
func (ps *promptService) PromptLocation(
ctx context.Context,
azureContext *AzureContext,
selectorOptions *SelectOptions,
) (*account.Location, error) {
if azureContext == nil {
azureContext = NewEmptyAzureContext()
}
if err := azureContext.EnsureSubscription(ctx); err != nil {
return nil, err
}
mergedOptions := &SelectOptions{}
if selectorOptions == nil {
selectorOptions = &SelectOptions{}
}
defaultOptions := &SelectOptions{
Message: "Select location",
LoadingMessage: "Loading locations...",
HelpMessage: "Choose an Azure location for your project.",
AllowNewResource: new(false),
}
if err := mergo.Merge(mergedOptions, selectorOptions, mergo.WithoutDereference); err != nil {
return nil, err
}
if err := mergo.Merge(mergedOptions, defaultOptions, mergo.WithoutDereference); err != nil {
return nil, err
}
// Get default location from user config
var defaultLocation = "eastus2"
userConfig, err := ps.userConfigManager.Load()
if err == nil {
userLocation, exists := userConfig.GetString("defaults.location")
if exists && userLocation != "" {
defaultLocation = userLocation
}
}
return PromptCustomResource(ctx, CustomResourceOptions[account.Location]{
cli/azd/pkg/prompt/prompt_service.go:431
- In
--no-promptmode,PromptResourceGroupwill callListResourceGroupand then fail via the interactive selector path (producing a prompt-message-only error) rather than returning a structuredinput.PromptRequiredErrorthat lists supported non-interactive sources (e.g.,AZURE_RESOURCE_GROUP). Consider short-circuiting whenps.globalOptions.NoPromptandazureContext.Scope.ResourceGroupis empty, returningPromptRequiredErrorwithInputs/sources and avoiding the ARM call.
// PromptResourceGroup prompts the user to select an Azure resource group.
func (ps *promptService) PromptResourceGroup(
ctx context.Context,
azureContext *AzureContext,
options *ResourceGroupOptions,
) (*azapi.ResourceGroup, error) {
if azureContext == nil {
azureContext = NewEmptyAzureContext()
}
if err := azureContext.EnsureSubscription(ctx); err != nil {
return nil, err
}
mergedSelectorOptions := &SelectOptions{}
if options == nil {
options = &ResourceGroupOptions{}
}
if options.SelectorOptions == nil {
options.SelectorOptions = &SelectOptions{}
}
defaultSelectorOptions := &SelectOptions{
Message: "Select resource group",
LoadingMessage: "Loading resource groups...",
HelpMessage: "Choose an Azure resource group for your project.",
AllowNewResource: new(true),
NewResourceMessage: "Create new resource group",
}
if err := mergo.Merge(mergedSelectorOptions, options.SelectorOptions, mergo.WithoutDereference); err != nil {
return nil, err
}
if err := mergo.Merge(mergedSelectorOptions, defaultSelectorOptions, mergo.WithoutDereference); err != nil {
return nil, err
}
return PromptCustomResource(ctx, CustomResourceOptions[azapi.ResourceGroup]{
- Files reviewed: 15/15 changed files
- Comments generated: 2
jongio
left a comment
There was a problem hiding this comment.
A few observations that complement what the existing review already flagged.
The biggest one isn't repeated here since it's already covered: promptService.PromptSubscription/PromptLocation/PromptResourceGroup no longer short-circuit in no-prompt mode and fall into PromptCustomResource, which goes interactive. Worth landing that fix before this merges.
The notes below are the parts I don't see covered yet.
NewEmptyAzureContext silently disables the new contract (cli/azd/pkg/prompt/prompt_models.go):
NewEmptyAzureContext doesn't carry globalOptions, so any context built through this helper has the no-prompt branch in Ensure* permanently disabled. promptService.PromptLocation and promptService.PromptResourceGroup both call NewEmptyAzureContext() when the caller passes nil, which means even after the no-prompt short-circuit gets added to those methods, the AzureContext path still won't surface PromptRequiredError for callers that let the service synthesize the context. Either accept globalOptions here too, or have the prompt service forward its own globalOptions into the empty context it creates.
Stale comment in gRPC handlers (cli/azd/internal/grpcserver/prompt_service.go, around the PromptSubscription/PromptLocation/PromptResourceGroup methods):
The "Delegate to prompt service which handles --no-prompt mode" comment is now misleading on those three handlers. After this PR the prompt service no longer handles --no-prompt for them (that's the regression flagged above), so the comment misleads the next reader. Either drop the "which handles --no-prompt mode" clause or, better, fix the underlying behavior and keep the comment honest.
Coverage gap on the new Ensure* no-prompt branches (cli/azd/pkg/prompt/azure_context_test.go):
The constructor calls got updated, but the new no-prompt branches in EnsureSubscription (~L190-203), EnsureLocation (~L258-271), and EnsureResourceGroup (~L224-238) don't have a direct test. The existing _NoPrompt cases in this file only check that a pre-set scope returns nil early, which never reaches the new PromptRequiredError return. A small per-method test that builds an AzureContext with &internal.GlobalCommandOptions{NoPrompt: true} and an empty scope, then asserts errors.AsType[*input.PromptRequiredError], would lock in the contract. Same pattern the new manager_test.go no-prompt tests use.
1490fe6 to
ff9902f
Compare
Azure Dev CLI Install InstructionsInstall scriptsMacOS/Linux
bash: pwsh: WindowsPowerShell install MSI install Standalone Binary
MSI
Documentationlearn.microsoft.com documentationtitle: Azure Developer CLI reference
|
|
/check-enforcer override |
This change standardizes handling and reporting of interactive input when
--no-promptis enabled.What changed
input.PromptRequiredErrorfor no-prompt failurespkg/prompt/prompt_service.goto require input for missing subscription, location, and resource group selectionpkg/infra/provisioning/manager.goto require input for missing subscription and location selectionWhy
Previously,
no-prompthandling was inconsistent throughout the system. This change aligns handling to a standard behavior, and provides failed cases with consistent human-readable and JSON output.This accomplishes the goal in #7743 of making
no-promptwork for true non-interactive work: CI and agents, and fixes the underlying concerns raised in #7390.Validation
Fixes #7743