Skip to content
Closed
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: 2 additions & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ words:
- protoimpl
- protojson
- protoreflect
- protowire
- anypb
- SNAPPROCESS
- structpb
- subtest
Expand Down
57 changes: 53 additions & 4 deletions cli/azd/cmd/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -1054,9 +1054,7 @@ type workflowCmdAdapter struct {
// ExecuteContext implements workflow.AzdCommandRunner.
// It rebuilds the cobra command tree on each call to ensure a clean slate,
// preventing "context canceled" errors from stale command state during retries.
// Global flags from the original process invocation are appended to the step args
// so that persistent flags (e.g., --trace-log-file) are properly parsed and visible
// to telemetry middleware on the fresh command tree.
// Inherited flags are merged into step args without overriding step flags.
func (w *workflowCmdAdapter) ExecuteContext(ctx context.Context, args []string) error {
// Cancel the child context when the step completes so that any event handlers
// registered during this step (e.g. by service target Initialize methods) are
Expand All @@ -1067,14 +1065,65 @@ func (w *workflowCmdAdapter) ExecuteContext(ctx context.Context, args []string)
rootCmd := w.newCommand()
// Always set args explicitly to prevent Cobra from falling back to os.Args[1:].
// Cobra uses os.Args when cmd.args is nil (but not when it's an empty slice).
mergedArgs := append(slices.Clone(args), w.globalArgs...)
mergedArgs := mergeWorkflowArgs(args, w.globalArgs)
if mergedArgs == nil {
mergedArgs = []string{}
}
rootCmd.SetArgs(mergedArgs)
return rootCmd.ExecuteContext(childCtx)
}

// mergeWorkflowArgs combines step and inherited flags. Step flags take
// precedence for both --name=value and --name value syntax.
func mergeWorkflowArgs(stepArgs, globalArgs []string) []string {
stepFlags := make(map[string]struct{})
for _, arg := range stepArgs {
if arg == "--" {
break
}

if name, ok := longFlagName(arg); ok {
stepFlags[name] = struct{}{}
}
}

merged := slices.Clone(stepArgs)
for i := 0; i < len(globalArgs); {
arg := globalArgs[i]
name, ok := longFlagName(arg)
end := i + 1
if ok && !strings.Contains(arg, "=") && end < len(globalArgs) &&
!strings.HasPrefix(globalArgs[end], "-") {
end++
}

if !ok {
merged = append(merged, globalArgs[i:end]...)
} else if _, shadowed := stepFlags[name]; !shadowed {
merged = append(merged, globalArgs[i:end]...)
}
i = end
}

return merged
}

func longFlagName(arg string) (string, bool) {
if !strings.HasPrefix(arg, "--") || len(arg) == 2 {
return "", false
}

name := strings.TrimPrefix(arg, "--")
if equal := strings.IndexByte(name, '='); equal >= 0 {
name = name[:equal]
}
if name == "" {
return "", false
}

return name, true
}

// extractGlobalArgs extracts global flag arguments from the process command line.
// It parses os.Args against the global flag set and returns only the flags that were
// explicitly set by the user, formatted as command-line arguments.
Expand Down
42 changes: 42 additions & 0 deletions cli/azd/cmd/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,48 @@ func Test_workflowCmdAdapter_ContextPropagation(t *testing.T) {
})
}

func TestMergeWorkflowArgs_StepFlagsShadowInheritedGlobals(t *testing.T) {
t.Parallel()

tests := []struct {
name string
stepArgs []string
globalArgs []string
expected []string
}{
{
name: "equals syntax",
stepArgs: []string{"deploy", "--output=none"},
globalArgs: []string{"--output=json", "--debug=true"},
expected: []string{"deploy", "--output=none", "--debug=true"},
},
{
name: "separate value syntax",
stepArgs: []string{"deploy", "--output", "none"},
globalArgs: []string{"--output", "json", "--debug=true"},
expected: []string{"deploy", "--output", "none", "--debug=true"},
},
{
name: "environment remains step scoped",
stepArgs: []string{"env", "set", "KEY", "VALUE", "--environment=child"},
globalArgs: []string{"--environment=parent", "--output=json"},
expected: []string{"env", "set", "KEY", "VALUE", "--environment=child", "--output=json"},
},
{
name: "environment is inherited when not overridden",
stepArgs: []string{"deploy"},
globalArgs: []string{"--environment=parent", "--output=json"},
expected: []string{"deploy", "--environment=parent", "--output=json"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, mergeWorkflowArgs(tt.stepArgs, tt.globalArgs))
})
}
}

func Test_NewRootCmd_ReregistrationReplacesProjectConfig(t *testing.T) {
// This test proves the regression from PR #7171: when workflowCmdAdapter called
// NewRootCmd (with full registration) for each workflow step, registerCommonDependencies
Expand Down
15 changes: 11 additions & 4 deletions cli/azd/docs/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,24 @@ specific version of the tool installed on the machine.
> **Note**: These variables are defined and consumed by individual azd extensions. As the extension
> ecosystem grows, extension-specific variables may move to each extension's own documentation.

### azure.ai.agents
### Microsoft Foundry extensions

The `azure.ai.projects` extension is the owner of Foundry project identity
values. The `azure.ai.agents` extension consumes those values when it creates
agent services and keeps its own agent-specific values.

| Variable | Description |
| --- | --- |
| `AZURE_AI_PROJECT_ID` | The Microsoft Foundry project resource ID used by the `azure.ai.agents` extension. |
| `FOUNDRY_PROJECT_ENDPOINT` | The Microsoft Foundry project endpoint used by the `azure.ai.agents` extension. Read first from the active azd environment and, if not present, from the host shell environment as an endpoint-resolution fallback. |
| `AZURE_AI_PROJECT_ID` | The Microsoft Foundry project resource ID resolved and persisted by `azure.ai.projects`. |
| `FOUNDRY_PROJECT_ENDPOINT` | The Microsoft Foundry project endpoint resolved and persisted by `azure.ai.projects`. `azure.ai.agents` reads it for agent workflows and endpoint-only compatibility. |
| `AZURE_AI_PROJECT_PRINCIPAL_ID` | The principal ID associated with the Microsoft Foundry project identity. |
| `AZURE_AI_ACCOUNT_NAME` | The Microsoft Foundry account name associated with the project. |
| `AZURE_AI_PROJECT_NAME` | The Microsoft Foundry project name. |
| `AZURE_AI_DEPLOYMENTS_LOCATION` | The location used to resolve and provision managed model deployments. |
| `AZURE_AI_MODEL_DEPLOYMENT_NAME` | The default model deployment name used for generated agent code and templates. |
| `AZURE_AI_PROJECT_ACR_CONNECTION_NAME` | The Azure Container Registry connection name used by the extension for hosted agents. |
| `AZURE_AI_PROJECT_CONNECTION_NAMES` | Comma-separated project connection names emitted by Foundry provisioning. |
| `AZURE_AI_PROJECT_CONNECTIONS_PROJECT_ENDPOINT` | The project endpoint used by connection services. |
| `AZURE_AI_PROJECT_ACR_CONNECTION_NAME` | The Azure Container Registry connection name used by hosted agents. |
| `AI_PROJECT_DEPLOYMENTS` | JSON-encoded deployment metadata populated by the extension for agent workflows. |
| `AI_PROJECT_DEPENDENT_RESOURCES` | JSON-encoded dependent resource metadata populated by the extension for agent workflows. |
| `AZD_AGENT_SKIP_ACR` | If `true`, signals the Bicep template to skip Azure Container Registry creation during provisioning. Automatically set by `azd agent init` for code-deploy scenarios (where no container image is built). |
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/extensions/azure.ai.agents/extension.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: Ship agents with Microsoft Foundry from your terminal. (Beta)
usage: azd ai agent <command> [options]
# NOTE: Make sure version.txt is in sync with this version.
version: 1.0.0-beta.9
requiredAzdVersion: ">=1.27.1"
requiredAzdVersion: ">=1.31.0-beta.1"
dependencies:
- id: azure.ai.inspector
version: "~1.0.0-beta.1"
Expand Down
Loading