Auto-install: fix/improve project extension requirement resolution - #9218
Conversation
|
Azure Pipelines: 20 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Adds preflight extension resolution so project commands install missing explicit and inferred providers before execution.
Changes:
- Resolves extension requirements and dependency graphs.
- Rebuilds the command tree after installation.
- Adds version-resolution and auto-install tests.
Show a summary per file
| File | Description |
|---|---|
cli/azd/pkg/extensions/manager.go |
Exposes extension version resolution. |
cli/azd/pkg/extensions/manager_test.go |
Tests nil metadata handling. |
cli/azd/cmd/auto_install.go |
Implements project extension preflight and installation. |
cli/azd/cmd/auto_install_test.go |
Tests requirements, providers, dependencies, and errors. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 5
- Review effort level: Medium
|
/azp run azure-dev - cli |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
Azure Pipelines: Successfully started running 1 pipeline(s). 21 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
📋 Prioritization NoteThanks for the contribution! The linked issue isn't in the current milestone yet. |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
Azure Pipelines: Successfully started running 1 pipeline(s). 21 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Review details
Comments suppressed due to low confidence (3)
cli/azd/cmd/project_extension_auto_install.go:292
- [azd-code-reviewer] Validate every dependency edge against the version already recorded here. If one branch selects
D <2and a later branch requiresD >=2, this early return silently accepts the conflict; installation then installs the first branch and fails on the later constraint, leaving extensions partially installed. Track the selected version/parent and surface incompatible constraints during preflight.
dependencyId := strings.ToLower(dependency.Id)
if _, isResolved := resolved[dependencyId]; isResolved {
continue
cli/azd/cmd/project_extension_auto_install.go:254
- [azd-code-reviewer] Apply the running azd version when selecting preflight versions. Passing
nilignoresrequiredAzdVersion, so an older azd can select and install an incompatible latest release (or reject a candidate whose latest release is incompatible even though a compatible release provides the provider). The normal extension-install path usescurrentAzdSemver()andInstallWithOptions; preflight selection and installation should use the same compatibility input.
version, err := extensions.ResolveExtensionVersion(
requirement.extension,
requirement.versionPreference,
nil,
cli/azd/cmd/project_extension_auto_install.go:234
- [azd-code-reviewer] Do not recommend
extension upgradefor every constraint mismatch. When the installed version is too new (for example, installed2.0.0with constraint<2.0.0), upgrading to latest cannot satisfy the requirement. The exact-version command is the reliable generic recovery path unless the constraint direction is inspected.
Suggestion: fmt.Sprintf(
"Run 'azd extension upgrade %s' to move to the latest version, or "+
"'azd extension install %s --version <version>' to select an exact version "+
"that satisfies %q.",
installed.Id,
installed.Id,
versionPreference,
),
- Files reviewed: 7/7 changed files
- Comments generated: 0 new
- Review effort level: Medium
jongio
left a comment
There was a problem hiding this comment.
Went through the delta since my last pass. Three things, one of them worth settling before this merges.
The consent guard removal is the main one. consoleCanPromptForConsent and its test are gone in this push, which puts --no-prompt and agent-detected sessions back on the unattended-install path, now ahead of every project command instead of only after a host already failed.
The docs file changed line endings, which turns a 26 line edit into a 676 line diff.
helpRequested treats help flags that are actually flag values as a help request.
The thread on cli/azd/pkg/infra/provisioning/provider.go:30 is still open from last round.
jongio
left a comment
There was a problem hiding this comment.
Two from this push.
Medium: resolveExtensionDependencies dropped the constraint checks it had in 398b133. One of those removals doesn't just omit a dependency, it records provider data for a version installation will never use, so a needed prompt gets skipped.
Low: the new ListInstalled failure path returns without setting result.Err, so the command exits 0 on an unsupported host.
Azure Dev CLI Install InstructionsInstall scriptsMacOS/Linux
bash: pwsh: WindowsPowerShell install MSI install Standalone Binary
MSI
Documentationlearn.microsoft.com documentationtitle: Azure Developer CLI reference
|
jongio
left a comment
There was a problem hiding this comment.
Two guards that went in earlier in this PR are gone from 3ed47a4 and both were doing real work. consoleCanPromptForConsent is the one I flagged on the last push and that thread is still open. commandWillRun is the second one, below.
cli/azd/cmd/project_extension_auto_install.go:641-azd up --invalidruns the full preflight and can install an extension before cobra rejects the flagcli/azd/cmd/project_extension_auto_install.go:596- every gated command resolves both provider kinds, soazd deployinstalls provisioning extensions it never loadscli/azd/pkg/extensions/manager.go:303- the provider filter selects a version without checking azd compatibility, so it can disagree withazd extension install
The legacy unsupported-host fallback returned without setting result.Err when ListInstalled failed, so a command that had already failed with UnsupportedServiceHostError exited 0. Scripts and CI key off the exit code. Hold the command failure in commandErr at the point of execution. The auto-install below declares its own err, which shadows the outer one, so assigning that err would have reported nil rather than the failure. Copilot-Session: b403388d-791f-40ab-843a-8f405475f308
There was a problem hiding this comment.
Review details
Comments suppressed due to low confidence (3)
cli/azd/cmd/project_extension_auto_install.go:241
- [azd-code-reviewer] Validate explicit requirements against the dependency graph before installing anything. For example, if
a.packdepends onz.ext1.x whilerequiredVersions.extensionsrequiresz.ext >=2, this best-effort walk returns no conflict;tryAutoInstallProjectExtensionsinstallsa.pack(and z.ext 1.x) first, then fails on the explicit z.ext requirement, leaving partial user state. The walk is also only invoked while resolving inferred providers, so an explicit-only project never checks this conflict. Return selected dependency versions/errors and reconcile them with all explicit constraints up front.
// resolveExtensionRequirementDependencies walks the dependencies of the required extensions so
// resolution can tell that installing one of them will pull in another. It is best effort: a
// dependency it cannot resolve is left out, which at worst prompts for an extension installation
// would have supplied anyway, and installation reports any genuine problem itself.
cli/azd/docs/extensions/extension-resolution-and-versioning.md:269
- This CI claim does not match the install path.
tryAutoInstallExtensionVersionreturns “Auto-installation is not supported in CI/CD environments” before it callsConfirm, even when--no-promptis set, so CI must install requirements manually. Either remove “and CI” here or change the CI guard intentionally.
Resolution only prompts for extensions that are genuinely missing, and it is skipped when the command renders help instead of running, such as `azd up --help`. Each install is confirmed before it happens; `--no-prompt` accepts that confirmation, matching how the rest of `azd` treats declared configuration in scripts and CI.
cli/azd/cmd/project_extension_auto_install.go:408
- [azd-code-reviewer] Use the running azd version when selecting provider versions. Passing
nilhere (and at the other newResolveExtensionVersioncalls) chooses the newest version even when itsrequiredAzdVersionexcludes the current CLI;tryAutoInstallExtensionVersionalso callsManager.Install, which omitsAzdVersion. This can install an incompatible release, unlikeazd extension install, or base provider discovery on a version azd should never select. ThreadcurrentAzdSemver()through discovery and install viaInstallWithOptions.
selectedVersion, err := extensions.ResolveExtensionVersion(extension, "", nil)
- Files reviewed: 7/7 changed files
- Comments generated: 0 new
- Review effort level: Medium
jongio
left a comment
There was a problem hiding this comment.
Incremental pass on 0e1ae08.
The comment you added on commandErr says every path out of that block has to report the failure. Two of them still don't, and one of those is newly reachable because of the filter added a few lines below it. Detail inline.
Low
result.Errleft unset on two exits from the unsupported-host fallback (cli/azd/cmd/auto_install.go:688)
Three exits from the fallback returned without setting result.Err, so a command that had already failed with UnsupportedServiceHostError exited 0: the FindExtensions error branch, the branch where nothing is installable, and the declined-install branch. The last one assigned the block-scoped err, which is always nil there because both branches that could leave it non-nil return above it. All three are only reachable once commandErr has been matched as an UnsupportedServiceHostError, so it is always non-nil at these sites and no successful run changes exit code. result.Err only selects the exit code in main.go, so nothing is printed twice. Copilot-Session: b403388d-791f-40ab-843a-8f405475f308
There was a problem hiding this comment.
Review details
Comments suppressed due to low confidence (6)
cli/azd/cmd/project_extension_auto_install.go:292
- [azd-code-reviewer] Track the selected dependency version and validate every incoming edge before skipping it. If
Adepends onBandD >=2whileBdepends onD <2, the first traversal cachesDand this edge is silently ignored; installation later installsB/Dand then failsA, leaving partial state instead of reporting the conflict up front.
dependencyId := strings.ToLower(dependency.Id)
if _, isResolved := resolved[dependencyId]; isResolved {
continue
cli/azd/cmd/project_extension_auto_install.go:298
- [azd-code-reviewer] An installed dependency that violates
dependency.Versioncannot be replaced by this registry selection.Manager.installInternalreuses installed dependencies and rejects the mismatch (pkg/extensions/manager.go:607-617), so falling through here records a newer provider as forthcoming and fails only after consent. Return the installed-version conflict during preflight instead.
// Installation reuses a compatible installed dependency instead of the registry selection.
installedDependency, err := extensionManager.GetInstalled(extensions.FilterOptions{Id: dependency.Id})
if err == nil && installedDependency != nil &&
versionSatisfiesConstraint(dependency.Id, installedDependency.Version, dependency.Version) {
cli/azd/cmd/auto_install.go:197
- [azd-code-reviewer] This recovery command drops an explicit requirement's version constraint. If the chosen source's latest release falls outside that constraint, the user installs the wrong version and the next project command immediately rejects it. For explicit requirements, include an exact satisfying
--versionselection or pass the requirement context into this guidance.
return nil, &internal.ErrorWithSuggestion{
Err: fmt.Errorf("more than one extension can be installed: %s", strings.Join(choices, ", ")),
Suggestion: "Run 'azd extension install <id> --source <source>' to select one, " +
"then run this command again.",
cli/azd/cmd/project_extension_auto_install.go:234
- [azd-code-reviewer]
upgradedoes not necessarily satisfy an arbitrary constraint. For an exact or upper-bounded constraint (for example<2.0.0) with v2 installed, upgrading leaves the project unsatisfied. Recommend only selecting an exact satisfying version unless registry data proves the latest version is valid.
Suggestion: fmt.Sprintf(
"Run 'azd extension upgrade %s' to move to the latest version, or "+
"'azd extension install %s --version <version>' to select an exact version "+
"that satisfies %q.",
cli/azd/docs/extensions/extension-resolution-and-versioning.md:269
- [azd-code-reviewer] This CI claim does not match the implementation:
tryAutoInstallExtensionVersionreturns “Auto-installation is not supported in CI/CD environments” before confirmation whenever a CI marker is present, even with--no-prompt. Either let no-prompt mode pass that CI guard or document that CI must install requirements manually.
Resolution only prompts for extensions that are genuinely missing, and it is skipped when the command renders help instead of running, such as `azd up --help`. Each install is confirmed before it happens; `--no-prompt` accepts that confirmation, matching how the rest of `azd` treats declared configuration in scripts and CI.
cli/azd/cmd/project_extension_auto_install.go:83
- [azd-code-reviewer] Cobra also renders help for boolean forms such as
--help=trueand--docs=true, but this exact-string scan misses them and can install extensions before the help short-circuit. Parse the boolean flag forms (while leaving=falseexecutable) so every help-only invocation remains side-effect free.
if arg == "-h" || arg == "--help" || arg == "--docs" {
return true
- Files reviewed: 7/7 changed files
- Comments generated: 0 new
- Review effort level: Medium
jongio
left a comment
There was a problem hiding this comment.
Incremental review of b33b6b7. The three fallback exits now set result.Err = commandErr, and the reasoning in the commit message holds up: errors.AsType returns false for a nil error, so commandErr is non-nil past line 640; UxMiddleware returns early for UnsupportedServiceHostError without printing it (cmd/middleware/ux.go:104-108); and main.go reads result.Err only to pick an exit code (lines 76 and 144). Nothing prints twice. I walked every return result in the function and none of them leave Err nil after a failed command now.
Two things from this pass:
- The
--no-promptambiguity error loses its suggestion before it reaches the user (auto_install.go:196) - Nothing tests the invariant this block keeps re-breaking (
auto_install.go:635)
Also confirmed #9354 is open and covers the two deferred requiredAzdVersion items, so those stay off this PR.
|
/check-enforcer override |
jongio
left a comment
There was a problem hiding this comment.
One new thing, and it's the same shape as the exit-code invariant I flagged last pass: a fix that shipped earlier in this PR isn't at HEAD any more, and the test that guarded it went with it. Details and a repro inline.
Still open from my last pass: the ErrorWithSuggestion at auto_install.go:196 never reaching the user, and the result.Err invariant at auto_install.go:635 living only in a comment.
Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com>
* Initial plan * Prepare azd core 1.28.2 release Co-authored-by: hemarina <104857065+hemarina@users.noreply.github.com> * Add #9218 to 1.28.2 changelog Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com> * Bump release version from 1.28.2 to 1.29.0 Co-authored-by: hemarina <104857065+hemarina@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: hemarina <104857065+hemarina@users.noreply.github.com> Co-authored-by: JeffreyCA <9157833+JeffreyCA@users.noreply.github.com>
Fixes #8999
This PR resolves the extension requirements declared or inferred from
azure.yamlbefore project commands run, soazdinstalls every missing provider once and then executes with a rebuilt command tree.Issue
Project commands could fail on the first unsupported service host before offering the required extension. Auto-install also ignored explicit extension requirements and custom provisioning providers, and duplicate registry sources could produce repeated or no-op source-selection prompts.
Extension resolution for project commands
Resolution runs before the project commands that resolve a provider (
up,provision,deploy,package,restore,down, andenv refresh), collecting explicitrequiredVersions.extensionsentries alongside the providers inferred from service hosts and infrastructure layers.azd up --helpandazd up --docsnever install extensions.azdimplements itself or that an installed extension already supplies, so an unreachable source cannot fail an ordinary project.--cwd, and skips resolution entirely when that directory does not exist yet, so a command never resolves the caller's unrelated project.--no-promptleaves no basis for choosing, rather than failing with a bare "prompt required".Version and dependency handling
Constraints declared in
azure.yamlare carried through to installation and validated up front rather than partway through.azdwould select publishes the provider, so a provider moved to another extension never causes an earlier version to be installed.Out of scope
azd up#9302 and will follow in a separate PR.--no-promptaccepts that confirmation the same way it does onmain. Requiring interactive consent would be a product-level change affecting every auto-install path, not just this one, and belongs in its own change.--output jsonbehavior is unchanged.azd showandazd monitoralready degrade to empty endpoints rather than failing, andazd infra generateperforms no provider resolution at all, so none of them resolve extensions.Example
The following project explicitly requires the Microsoft Foundry extension pack while also using service and provisioning providers supplied by its dependencies:
Without changes:
With changes:
Behavior at a glance
requiredVersions.extensionsinfra.providerazd up --help--cwdpointing at a directory that does not exist yet--no-promptprompt requiredTesting
Covered explicit constraints, provider version selection, installed and built-in providers, duplicate sources, shared service and infrastructure providers, pack dependency graphs including transitive, unresolvable, and cyclic edges, deterministic ordering, and visible resolution failures. Also covered the help short-circuit and the command scope predicate. Validated the affected packages with build, unit-test, lint, and spell checks, plus isolated CLI runs against real registry metadata.