feat: add azd tool command group for unified Azure tooling management#7450
feat: add azd tool command group for unified Azure tooling management#7450wbreza wants to merge 14 commits intoAzure:mainfrom
Conversation
jongio
left a comment
There was a problem hiding this comment.
POC for the azd tool command group - structure is clean and well-decomposed across manifest, detector, installer, manager, and update checker.
Issues to address:
- pkg/tool/installer.go:389 - splitCommand can't handle shell pipes, breaking Linux az-cli install
- cli/azd/cover - test coverage output file accidentally committed
- pkg/tool/manifest.go:247 - MCP server detection via npx downloads packages on every detection call
- cmd/tool.go:522 - upgrade action pre-executes then creates a task list for display only
- cmd/root.go:546 - isWorkflowCommand uses a hardcoded map instead of existing command group annotations
|
@jongio - review again please. |
jongio
left a comment
There was a problem hiding this comment.
Previous comments addressed. Two new findings on the latest code:
- pkg/tool/detector.go -
detectCommandBasedreports Server/Library tools (MCP server, azd AI extensions) as installed when only the shared binary (npm, azd) is on PATH, even when the specific package isn't installed - cmd/middleware/tool_first_run.go and tool_update_check.go have no unit tests despite complex skip logic and background goroutine management
One nit:
- cmd/tool.go - install strategies in
showcommand display in non-deterministic map iteration order
jongio
left a comment
There was a problem hiding this comment.
Previous comments addressed. A few new edge cases:
- cmd/middleware/tool_first_run.go:170 - detection failure permanently kills the first-run experience
- pkg/tool/installer.go:194 - ensurePlatform lazy cache has no synchronization
- pkg/tool/update_checker.go:176 - cache read errors silently discarded
One test gap: no coverage for the commandBased false-positive fix (Server/Library tool with VersionRegex set + non-matching output should report not installed).
Implements the azd tool POC — a unified CLI for discovering, installing, upgrading, and checking status of Azure development tools. New command group: - azd tool — Interactive tool discovery and install flow - azd tool list — List all tools with status (table/JSON) - azd tool install — Install tools by name or interactively - azd tool upgrade — Upgrade installed tools - azd tool check — Check for available updates (table/JSON) - azd tool show — Show detailed info for a specific tool Core package (pkg/tool/): - manifest.go — Tool definitions and built-in registry (7 tools) - platform.go — OS detection and package manager availability - detector.go — Tool detection (CLI version parsing, VS Code extensions) - installer.go — Platform-aware install/upgrade orchestration - manager.go — Top-level orchestrator - update_checker.go — Periodic update checking with disk cache Middleware: - tool_first_run.go — Welcome experience on first azd use - tool_update_check.go — Background update notifications Integration: - IoC registration in container.go - Command registration in root.go - Snapshot test updates for new commands Unit tests: 45 tests, 83.9% coverage on pkg/tool/ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Use American English spellings (summarizes, serialized, amortized, unparsable) - Add azureresourcegroups to cspell overrides (VS Code extension ID) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace legacy console.MultiSelect/Confirm/Spinner (survey.v2) with the newer pkg/ux/ components that use canvas-based rendering. This fixes escape code artifacts on arrow key presses in terminal. Changes: - cmd/tool.go: 2x console.MultiSelect -> uxlib.NewMultiSelect - cmd/middleware/tool_first_run.go: console.Confirm -> uxlib.NewConfirm, console.MultiSelect -> uxlib.NewMultiSelect, console.ShowSpinner/StopSpinner -> uxlib.NewSpinner.Run Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add 30s timeout to background update check goroutine to prevent leaks - Fix middleware predicate to properly exclude tool subcommands - Defer GetUserConfigDir() call from IoC registration to first use - Reorder middleware skip checks (cheap before expensive I/O) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add resource.IsRunningOnCI() check to both tool middlewares, matching the pattern used by LoginGuardMiddleware - Change middleware predicates from blocklist to allowlist of workflow commands (init, up, deploy, provision, build, package, restore, publish, down, monitor, add) - Utility commands (auth, config, env, show, etc.) no longer trigger the first-run experience or update notifications Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix shell pipe handling in installer splitCommand for commands containing | or redirects (e.g., curl | sudo bash) - Remove accidentally committed test coverage file, add to .gitignore - Change MCP server detection from npx (triggers download) to npm list -g (local check only) - Move upgrade execution inside TaskList callbacks so spinners reflect real-time progress - Replace hardcoded workflow command allowlist with group annotation check (CmdGroupStart/CmdGroupAzure/CmdGroupBeta) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add nolint:gosec directive for intentional context.Background() in background goroutine (tool_update_check.go) - Add GOWORK/Gowork to cspell dictionary (magefile.go references) - Update TestUsage-azd.snap to include new build command Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Test_RunMethodsNoBareErrors requires all action Run() methods to use sentinel-wrapped errors (fmt.Errorf with %w) for proper telemetry classification. Fixed bare fmt.Errorf calls in tool upgrade action. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add errors.Is check for ErrToolUpgradeFailed in classifySentinel to satisfy Test_PackageLevelErrorsMapped which requires all package-level error sentinels to be mapped for telemetry classification. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- detectCommandBased now only sets Installed=true when version regex matches, consistent with detectExtension pattern. Fixes false positives for package-based tools (npm, azd) where the binary exists but the specific package may not be installed. - Sort install strategy display by platform key for deterministic output in 'azd tool show'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Only mark first-run completed on success/explicit skip, not on transient detection failures - Use sync.Once for thread-safe platform lazy-init in installer - Log and clean up corrupt cache files in update checker Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Merge conflict markers from rebase were left in .vscode/cspell.yaml, causing both cspell CI checks to fail. Merged both sides correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.
Round 4 - three new findings in code paths previous rounds didn't cover.
Smaller items (not blocking):
-
detectCommandBasedtest gap: there's no test case for "VersionRegex is set but output doesn't match - tool should NOT be installed." The round 3 fix (gatingInstalledon regex match) works correctly, but a regression test inTestDetectTool_CommandBasedwould lock it down. -
HasUpdatesAvailable(update_checker.go) counts uninstalled tools as needing updates. The conditionlatest != s.InstalledVersionis true when the tool isn't installed (InstalledVersionis empty) butlatestis non-empty. Currently unreachable in the POC since no remote API populatesLatestVersion, but it'll bite when that's added. Addings.Installedto the guard fixes it. -
Middleware test coverage:
tool_first_run.go(327 lines) andtool_update_check.go(176 lines) still have zero tests despite complex skip logic, CI detection, and background goroutine management. Mentioned this in round 2 body but calling it out again.
| DetectCommand: "code", | ||
| VersionArgs: []string{"--list-extensions", "--show-versions"}, | ||
| VersionRegex: `ms-azuretools\.vscode-azureresourcegroups@(\d+\.\d+\.\d+)`, | ||
| InstallStrategies: allPlatforms(InstallStrategy{ |
There was a problem hiding this comment.
VS Code extension strategies set PackageManager: "code" + InstallCommand but no PackageId. Two consequences:
-
buildCodeCommandin installer.go is unreachable dead code -buildCommand's priority chain always hits theInstallCommandpath before reachingPackageManager + PackageId. -
Upgrades don't get
--force. The upgrade path inbuildCommandrequiresPackageManager != ""ANDPackageId != "". WithoutPackageId, upgrades fall through to theInstallCommandpath, which runs the same install command without--force. Sincecode --install-extensionwithout--forcewon't update an already-installed extension,azd tool upgradeis silently a no-op for VS Code extensions.
Fix for all three: add PackageId with the extension identifier and drop InstallCommand:
InstallStrategies: allPlatforms(InstallStrategy{
PackageManager: "code",
PackageId: "ms-azuretools.vscode-azureresourcegroups",
}),Same for vscodeBicep ("ms-azuretools.vscode-bicep") and vscodeGitHubCopilot ("GitHub.copilot"). This makes buildCodeCommand reachable, upgrades get --force, and the manager availability check gives a friendly error when VS Code isn't installed.
| if installErr != nil { | ||
| return uxlib.Error, installErr | ||
| } | ||
| if len(results) > 0 && !results[0].Success { |
There was a problem hiding this comment.
Task callbacks check results[0].Success but InstallTools returns dependency results first. If a tool has uninstalled dependencies (currently only azd-ai-extensions depends on az-cli), results[0] is the dependency's result, not the requested tool's. When the dependency succeeds but the primary tool fails, the task incorrectly reports success.
The middleware's offerInstall in tool_first_run.go already handles this correctly with a for _, r := range results loop.
Fix - find the result matching the requested tool:
for _, r := range results {
if r.Tool != nil && r.Tool.Id == capturedID {
if !r.Success {
return uxlib.Error, r.Error
}
break
}
}Apply to both occurrences: toolAction.Run (~line 224) and toolInstallAction.Run (~line 402).
| // showNotificationIfNeeded displays a one-line update notification | ||
| // when cached results indicate that tool updates are available and | ||
| // the current console session is interactive. | ||
| func (m *ToolUpdateCheckMiddleware) showNotificationIfNeeded(ctx context.Context) { |
There was a problem hiding this comment.
showNotificationIfNeeded calls HasUpdatesAvailable then MarkUpdateNotificationShown but never calls ShouldShowNotification. That method exists on UpdateChecker but isn't exposed through Manager, so it's dead code. This means the update notification is shown on every interactive invocation that has cached updates, rather than once per check cycle as intended.
Fix: add a ShouldShowNotification method to Manager (delegating to UpdateChecker.ShouldShowNotification), then call it in showNotificationIfNeeded before the HasUpdatesAvailable check:
func (m *ToolUpdateCheckMiddleware) showNotificationIfNeeded(ctx context.Context) {
if m.shouldSkipNotification() {
return
}
if !m.manager.ShouldShowNotification(ctx) {
return
}
// ... rest of the method
}
Summary
Adds
azd tool— a unified command group for discovering, installing, upgrading, and managing Azure development tools directly from the CLI. No more hunting for install docs or remembering package manager commands across platforms.What You Can Do
Discover your tool status at a glance
Interactive setup (just run
azd tool)$ azd tool Checking installed Azure development tools... Installed: ✓ Azure CLI (az) v2.67.0 ✓ Azure Developer CLI (azd) v1.12.0 (that's you!) Available: ○ GitHub Copilot CLI Not installed (recommended) ○ Bicep (VS Code Extension) Not installed (recommended) ? Select tools to install: > [✔] GitHub Copilot CLI (recommended) | [✔] Bicep (VS Code Extension) (recommended) | [ ] Azure MCP Server (optional)Install specific tools by name
Install all recommended tools at once
Upgrade installed tools
Check for available updates
View details for a specific tool
$ azd tool show az-cli Name: Azure CLI Category: cli Priority: recommended Website: https://learn.microsoft.com/cli/azure/ Installed: v2.67.0 Install strategies: Windows: winget install Microsoft.AzureCLI macOS: brew install azure-cli Linux: curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bashJSON output for scripting
Built-in Tools (7)
az)codeCLIcodeCLIcodeCLIFirst-Run Experience
On your first workflow command (
azd init,azd up,azd deploy, etc.), azd offers to check your development tools — a one-time prompt that can be skipped with--no-prompt,AZD_SKIP_FIRST_RUN=true, or in CI environments.CI/Non-Interactive
All commands work in CI pipelines:
--no-prompt/AZD_NON_INTERACTIVE=truesuppresses all promptsCI,TF_BUILD,GITHUB_ACTIONS) are auto-detected--output json) for machine consumptioninstall/upgrade(no interactive selection needed)Testing
pkg/tool/with 83.9% statement coverage