Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ overrides:
- filename: extensions/azure.ai.models/internal/cmd/custom_create.go
words:
- Qwen
- filename: pkg/infra/provisioning/bicep/local_preflight.go
- filename: pkg/infra/provisioning/bicep/provision_validation.go
words:
- actioned
- filename: pkg/project/project.go
Expand Down
4 changes: 2 additions & 2 deletions cli/azd/cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,7 @@ func Test_EnvSetSecretAction_VaultNotProvisioned_Cancel(t *testing.T) {

_, err := action.Run(t.Context())
require.Error(t, err)
assert.Contains(t, err.Error(), "operation cancelled by user")
assert.Contains(t, err.Error(), "operation canceled by user")
}

func Test_EnvSetSecretAction_VaultNotProvisioned_SelectError(t *testing.T) {
Expand Down Expand Up @@ -1559,7 +1559,7 @@ func Test_EnvSetSecretAction_VaultDefinedButNotProvisioned(t *testing.T) {

_, err := action.Run(t.Context())
require.Error(t, err)
require.Contains(t, err.Error(), "cancelled")
require.Contains(t, err.Error(), "canceled")
}

func Test_SelectDistinctExtension_ZeroMatches(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/cmd/middleware/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ func Test_ShouldSkipAgentHandling_ControlFlow(t *testing.T) {

t.Run("Wrapped ErrAbortedByUser is skipped", func(t *testing.T) {
t.Parallel()
wrapped := fmt.Errorf("preflight declined: %w", internal.ErrAbortedByUser)
wrapped := fmt.Errorf("validation declined: %w", internal.ErrAbortedByUser)
require.True(t, shouldSkipAgentHandling(wrapped))
})

Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cli/azd/docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ integration.
| `AZD_DEPLOY_CONCURRENCY` | Maximum number of services to deploy in parallel during `azd deploy`. Only takes effect when at least one service declares `uses:` targeting another service; without `uses:` edges, services deploy sequentially in alphabetical order for backward compatibility (see [concurrency model](concurrency-model.md)). Parsed as a positive integer; clamped to a maximum of `64`. When unset, concurrency is unlimited (bounded only by the number of services). |
| `AZD_DEPLOY_TIMEOUT` | Timeout for deployment operations, parsed as an integer number of seconds (for example, `1200`). Defaults to `1200` seconds (20 minutes). |
| `AZD_PROVISION_CONCURRENCY` | Maximum number of infrastructure layers to provision in parallel during `azd provision`. Parsed as a positive integer; clamped to a maximum of `64`. When unset, concurrency is unlimited (bounded only by the dependency graph). |
| `AZD_DEPLOYMENT_ID_FILE` | Absolute path of a file where `azd` writes ARM deployment IDs in NDJSON format (one JSON line per layer) during `azd provision` or `azd up`. The file is truncated at the start of each provisioning run, and each infrastructure layer appends one line as its ARM deployment starts. Each line has the shape `{"deploymentId":"/subscriptions/.../deployments/<name>","layer":"<layer-name>"}` — the `layer` field is empty for non-layered (single-module) provisioning. Consumers should tail/watch the file and parse each line independently; unknown fields must be ignored for forward compatibility. The path must be absolute (relative paths are ignored); the containing directory must already exist and be writable. Lines are only appended when an ARM deployment is actually started — runs short-circuited by the deployment-state cache or aborted by preflight do not produce output. A process-wide mutex serializes writes so each line is always complete. If the file cannot be written (for example, the parent directory does not exist, the path is not writable, or the path points to a directory rather than a file), provisioning continues and the failure is recorded via the standard log; that output is only visible when `--debug` or `AZD_DEBUG_LOG` is enabled. On Windows, consumers should use a file-watcher pattern that does not keep a read handle open, otherwise new appends may fail. Only Bicep deployments are supported. |
| `AZD_DEPLOYMENT_ID_FILE` | Absolute path of a file where `azd` writes ARM deployment IDs in NDJSON format (one JSON line per layer) during `azd provision` or `azd up`. The file is truncated at the start of each provisioning run, and each infrastructure layer appends one line as its ARM deployment starts. Each line has the shape `{"deploymentId":"/subscriptions/.../deployments/<name>","layer":"<layer-name>"}` — the `layer` field is empty for non-layered (single-module) provisioning. Consumers should tail/watch the file and parse each line independently; unknown fields must be ignored for forward compatibility. The path must be absolute (relative paths are ignored); the containing directory must already exist and be writable. Lines are only appended when an ARM deployment is actually started — runs short-circuited by the deployment-state cache or canceled by provision validation do not produce output. A process-wide mutex serializes writes so each line is always complete. If the file cannot be written (for example, the parent directory does not exist, the path is not writable, or the path points to a directory rather than a file), provisioning continues and the failure is recorded via the standard log; that output is only visible when `--debug` or `AZD_DEBUG_LOG` is enabled. On Windows, consumers should use a file-watcher pattern that does not keep a read handle open, otherwise new appends may fail. Only Bicep deployments are supported. |
| `AZD_UP_CONCURRENCY` | Maximum number of steps to run in parallel during `azd up`. Parsed as a positive integer; clamped to a maximum of `64`. Falls back to `AZD_DEPLOY_CONCURRENCY` when unset. When both are unset, concurrency is unlimited. |
| `AZD_DEPLOY_{SERVICE}_SLOT_NAME` | Sets the App Service deployment slot target for a service. Replace `{SERVICE}` with the uppercase service name (hyphens become underscores). Set to `production` to deploy to the main app, or a slot name (e.g., `staging`). When slots exist and this is not set, `--no-prompt` mode fails with an error listing available targets. |
| `AZD_DEPLOY_{SERVICE}_SKIP_STATUS_CHECK` | If `true`, skips runtime deployment status tracking for the named Linux App Service after zip deploy. Useful when the target web app is intentionally stopped. Parsed as a boolean (`true`/`false`/`1`/`0`). `{SERVICE}` follows the same naming rules as `AZD_DEPLOY_{SERVICE}_SLOT_NAME`. |
Expand Down
6 changes: 3 additions & 3 deletions cli/azd/docs/extensions/extension-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -964,7 +964,7 @@ Extensions can provide AI agent tools through the Model Context Protocol, enabli
Extensions can contribute validation checks to azd's validation pipeline. Currently
supported check types:

- **`local-preflight`** — Bicep-only checks run during `azd provision` before
- **`arm-provision`** — Bicep-only checks run during `azd provision` before
deployment. The extension receives the Bicep snapshot, ARM template, ARM
parameters, and Azure location as context. These only fire for Bicep-provisioned
deployments.
Expand All @@ -990,7 +990,7 @@ host := azdext.NewExtensionHost(azdClient).
})
```

See [`local-preflight-validation.md`](../design/local-preflight-validation.md#extension-provided-checks)
See [`provision-validation.md`](../design/provision-validation.md#extension-provided-checks)
for full details on the check interface and context keys.

#### Future Considerations
Expand Down Expand Up @@ -1102,7 +1102,7 @@ Extensions can declare the following capabilities in their manifest:
- **`service-target-provider`**: Provide custom service deployment targets
- **`framework-service-provider`**: Provide custom language frameworks and build systems
- **`provisioning-provider`**: Provide a custom infrastructure provisioning experience (alternative to Bicep / Terraform)
- **`validation-provider`**: Contribute validation checks to azd's preflight and future validation pipelines
- **`validation-provider`**: Contribute validation checks to azd's provision validation and future validation pipelines
- **`metadata`**: Provide comprehensive metadata about commands and configuration schemas

#### Complete Extension Manifest Example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func configureExtensionHost(host *azdext.ExtensionHost) {
// Provider-agnostic check: runs before provisioning regardless of the
// provisioning provider. This matters because the azure.ai.agents
// extension provisions through its own microsoft.foundry provider,
// which never triggers the Bicep-only "local-preflight" checks.
// which never triggers the Bicep-only "arm-provision" checks.
CheckType: azdext.ValidationCheckTypeProvision,
RuleID: project.ResourceGroupLocationRuleID,
Factory: func() azdext.ValidationCheckProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ const ResourceGroupLocationRuleID = "azure.ai.agents.resource_group_location_mis
// ResourceGroupLocationCheck is a provider-agnostic "provision" validation check
// (azdext.ValidationCheckTypeProvision) that detects an immutable resource-group
// region conflict before provisioning starts. It is registered under the
// provision check type — rather than the Bicep-only "local-preflight" type —
// provision check type — rather than the Bicep-only "arm-provision" type —
// because the azure.ai.agents extension provisions through its own
// microsoft.foundry provider, which never triggers local-preflight checks.
// microsoft.foundry provider, which never triggers arm-provision checks.
//
// The azure.ai.agents extension writes a stable, salted resource group name to
// AZURE_RESOURCE_GROUP at init. If AZURE_LOCATION is later changed (for example by
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ func configureExtensionHost(host *azdext.ExtensionHost) {
return project.NewDemoProvisioningProvider(azdClient)
}).
WithValidationCheck(azdext.ValidationCheckRegistration{
// Bicep-only check: runs during BicepProvider preflight and
// receives the Bicep snapshot / ARM template context. It is
// Bicep-only check: runs during BicepProvider provision validation
// and receives the Bicep snapshot / ARM template context. It is
// skipped gracefully when no snapshot is available (e.g. a
// non-Bicep provider), but is not dead code for Bicep.
CheckType: azdext.ValidationCheckTypeLocalPreflight,
CheckType: azdext.ValidationCheckTypeArmProvision,
RuleID: "demo_warning",
Factory: func() azdext.ValidationCheckProvider {
return project.NewDemoValidationCheck()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ func TestDemoValidationCheck_AlwaysReturnsWarning(t *testing.T) {
t.Run("no ARM template", func(t *testing.T) {
valCtx := &azdext.ValidationContext{
ContextID: "test-ctx",
CheckType: azdext.ValidationCheckTypeLocalPreflight,
CheckType: azdext.ValidationCheckTypeArmProvision,
Data: map[string][]byte{},
}
req := &azdext.ValidationCheckRequest{
CheckType: azdext.ValidationCheckTypeLocalPreflight,
CheckType: azdext.ValidationCheckTypeArmProvision,
RuleId: "demo_warning",
ContextId: "test-ctx",
}
Expand All @@ -42,13 +42,13 @@ func TestDemoValidationCheck_AlwaysReturnsWarning(t *testing.T) {
resourcesJSON := []byte(`[{"type": "a"}, {"type": "b"}, {"type": "c"}]`)
valCtx := &azdext.ValidationContext{
ContextID: "test-ctx-2",
CheckType: azdext.ValidationCheckTypeLocalPreflight,
CheckType: azdext.ValidationCheckTypeArmProvision,
Data: map[string][]byte{
azdext.ValidationContextPredictedResources: resourcesJSON,
},
}
req := &azdext.ValidationCheckRequest{
CheckType: azdext.ValidationCheckTypeLocalPreflight,
CheckType: azdext.ValidationCheckTypeArmProvision,
RuleId: "demo_warning",
ContextId: "test-ctx-2",
}
Expand Down
4 changes: 2 additions & 2 deletions cli/azd/grpc/proto/validation.proto
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ message ValidationMessage {
// RegisterValidationCheckRequest is sent by extensions to register a validation check.
message RegisterValidationCheckRequest {
// check_type identifies the validation context this check targets
// (e.g. "local-preflight", "project-config"). Future check types can be
// added without changing the protocol.
// (e.g. "provision", "arm-provision", "project-config"). Future check
// types can be added without changing the protocol.
string check_type = 1;
// rule_id is a stable, unique identifier for this check rule,
// used in telemetry and diagnostics.
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/internal/cmd/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1202,7 +1202,7 @@ func Test_PackageLevelErrorsMapped(t *testing.T) {
"ErrRemoteHostIsNotGitHub": "caught in pkg/pipeline and pkg/github before reaching telemetry",
"ErrSSHNotSupported": "only defined, referenced via ErrRemoteHostIsNotAzDo flow",
"ErrDeploymentNotFound": "caught in provisioning/deployment callers before reaching telemetry",
"ErrProvisionValidationAborted": "translated to internal.ErrAbortedByUser by wrapProvisionError " +
"ErrProvisionValidationCanceled": "translated to internal.ErrAbortedByUser by wrapProvisionError " +
"before reaching telemetry",
"ErrDeploymentsNotFound": "caught in infra callers before reaching telemetry",
"ErrDeploymentResourcesNotFound": "caught in infra callers before reaching telemetry",
Expand Down
36 changes: 18 additions & 18 deletions cli/azd/internal/cmd/provision_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ import (
"go.uber.org/multierr"
)

// errPreflightAbortedByUser is a sentinel returned from [provisionSingleLayer]
// when the underlying provider reports [provisioning.PreflightAbortedSkipped].
// errValidationCanceledByUser is a sentinel returned from [provisionSingleLayer]
// when the underlying provider reports [provisioning.ProvisionValidationCanceledSkipped].
// The caller translates it to [internal.ErrAbortedByUser] at the action
// boundary so the user sees a friendly "Provisioning was cancelled." message.
var errPreflightAbortedByUser = errors.New("provisioning aborted by user during preflight")
// boundary so the user sees a friendly "Provisioning was canceled." message.
var errValidationCanceledByUser = errors.New("provisioning canceled by user during provision validation")

// provisionLayersGraph is the single execution entry point for
// [ProvisionAction]. It dispatches to one of four disjoint paths:
Expand Down Expand Up @@ -93,7 +93,7 @@ func (p *ProvisionAction) provisionLayersGraph(
// warning confirmation prompt — once per layer with an identical,
// env-scoped context. On abort (or a prompt failure), translate through
// wrapProvisionError so preview and deploy share the same UX: an abort
// becomes the "Provisioning was cancelled." message.
// becomes the "Provisioning was canceled." message.
if err := p.provisionManager.RunProvisionValidation(ctx, previewMode); err != nil {
return nil, p.wrapProvisionError(ctx, err)
}
Expand Down Expand Up @@ -166,11 +166,11 @@ func (p *ProvisionAction) provisionLayersGraph(
return hookErr
}

if deployResult.SkippedReason == provisioning.PreflightAbortedSkipped {
if deployResult.SkippedReason == provisioning.ProvisionValidationCanceledSkipped {
// Return the internal sentinel; wrapProvisionError at the
// outer boundary emits the "Provisioning was cancelled."
// outer boundary emits the "Provisioning was canceled."
// UX message and translates to internal.ErrAbortedByUser.
return errPreflightAbortedByUser
return errValidationCanceledByUser
}

skipped := deployResult.SkippedReason == provisioning.DeploymentStateSkipped
Expand Down Expand Up @@ -301,7 +301,7 @@ func (p *ProvisionAction) provisionLayersGraph(
if result.Error != nil {
// Peel the scheduler's `step "X" failed:` prefix and run the
// underlying error through the same wrapping used by the sequential
// path (OpenAI / Responsible AI translation, preflight-abort
// path (OpenAI / Responsible AI translation, provision-validation-cancel
// ErrAbortedByUser, state dump on provider failure, etc.).
return nil, p.wrapProvisionError(ctx, unwrapStepErrors(result))
}
Expand Down Expand Up @@ -649,7 +649,7 @@ func (p *ProvisionAction) logProvisionGraphTimings(result *exegraph.RunResult) {

// wrapProvisionError replicates the sequential path's error-wrapping logic
// (provision.go:382-435): JSON state dump on failure, OpenAI access wrapper,
// Responsible AI wrapper, and the preflight-aborted translation.
// Responsible AI wrapper, and the provision-validation-canceled translation.
func (p *ProvisionAction) wrapProvisionError(ctx context.Context, err error) error {
return wrapProvisionError(ctx, err, provisionErrorDeps{
console: p.console,
Expand Down Expand Up @@ -677,11 +677,11 @@ type provisionErrorDeps struct {
// provisionManager (the same one used for the primary layer) so that the
// JSON state dump on failure has something to render.
func wrapProvisionError(ctx context.Context, err error, deps provisionErrorDeps) error {
// Preflight-aborted → ErrAbortedByUser with success message.
if errors.Is(err, errPreflightAbortedByUser) ||
errors.Is(err, provisioning.ErrProvisionValidationAborted) {
// Validation-canceled → ErrAbortedByUser with success message.
if errors.Is(err, errValidationCanceledByUser) ||
errors.Is(err, provisioning.ErrProvisionValidationCanceled) {
deps.console.MessageUxItem(ctx, &ux.ActionResult{
SuccessMessage: "Provisioning was cancelled.",
SuccessMessage: "Provisioning was canceled.",
})
return internal.ErrAbortedByUser
}
Expand Down Expand Up @@ -865,8 +865,8 @@ func provisionSingleLayer(
// parallel and B's clone may pre-date A's reload.
//
// Returns the raw [provisioning.DeployResult] so callers can record skip
// semantics; on [provisioning.PreflightAbortedSkipped] it returns
// [errPreflightAbortedByUser] so [ProvisionAction] can translate it to
// semantics; on [provisioning.ProvisionValidationCanceledSkipped] it returns
// [errValidationCanceledByUser] so [ProvisionAction] can translate it to
// [internal.ErrAbortedByUser].
func runProvisionSingleLayer(
ctx context.Context,
Expand Down Expand Up @@ -976,8 +976,8 @@ func runProvisionSingleLayer(
return nil, fmt.Errorf("deploying layer %s: %w", stepName, err)
}

if deployResult.SkippedReason == provisioning.PreflightAbortedSkipped {
return deployResult, errPreflightAbortedByUser
if deployResult.SkippedReason == provisioning.ProvisionValidationCanceledSkipped {
return deployResult, errValidationCanceledByUser
}

// ── Step 4: Env merge ──
Expand Down
10 changes: 5 additions & 5 deletions cli/azd/internal/cmd/provision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,23 +113,23 @@ func (p *mockProvider) PlannedOutputs(_ context.Context) ([]provisioning.Planned
return nil, nil
}

// TestProvisionAction_PreflightAborted verifies that when the user declines
// preflight warnings, ProvisionAction.Run returns ErrAbortedByUser and does NOT
// TestProvisionAction_ProvisionValidationCanceled verifies that when the user declines
// provision validation warnings, ProvisionAction.Run returns ErrAbortedByUser and does NOT
// attempt to read deployResult.Deployment.Outputs (which would nil-panic).
//
// Regression test for https://github.com/Azure/azure-dev/issues/7305
func TestProvisionAction_PreflightAborted(t *testing.T) {
func TestProvisionAction_ProvisionValidationCanceled(t *testing.T) {
t.Parallel()
// Set up a temp project with a minimal infra directory so ImportManager works.
projectDir := t.TempDir()
infraDir := filepath.Join(projectDir, "infra")
require.NoError(t, os.MkdirAll(infraDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(infraDir, "main.bicep"), []byte("targetScope = 'subscription'\n"), 0o600))

// Mock provider that simulates preflight abort (user said No).
// Mock provider that simulates provision validation cancel (user said No).
provider := &mockProvider{
deployResult: &provisioning.DeployResult{
SkippedReason: provisioning.PreflightAbortedSkipped,
SkippedReason: provisioning.ProvisionValidationCanceledSkipped,
},
}

Expand Down
2 changes: 1 addition & 1 deletion cli/azd/internal/cmd/up_graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func (u *UpGraphAction) Run(
// once rather than once per concurrently-provisioned layer. Skipped when
// there are no provision layers (nothing to provision). On abort (or a
// prompt failure), surface the shared UX via wrapProvisionError — an abort
// becomes the "Provisioning was cancelled." message.
// becomes the "Provisioning was canceled." message.
if len(layers) > 0 {
if err := u.provisionManager.RunProvisionValidation(ctx, false); err != nil {
return nil, wrapProvisionError(ctx, err, provisionErrorDeps{
Expand Down
Loading
Loading