Skip to content

fix(auth): resolve az via shutil.which so azure-cli token works on Windows#3709

Open
jawwad-ali wants to merge 2 commits into
github:mainfrom
jawwad-ali:fix/azure-cli-resolve-az-executable
Open

fix(auth): resolve az via shutil.which so azure-cli token works on Windows#3709
jawwad-ali wants to merge 2 commits into
github:mainfrom
jawwad-ali:fix/azure-cli-resolve-az-executable

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

Problem

AzureDevOpsAuth._acquire_via_az_cli() runs:

result = subprocess.run(["az", "account", "get-access-token", ...], ...)

with a bare "az". On Windows the Azure CLI is installed as az.cmd, and subprocess.run calls CreateProcess, which — unlike a shell — does not consult PATHEXT. So a bare "az" fails with WinError 2 (FileNotFoundError) even when az works fine in the user's terminal after az login. The except OSError swallows it, so auth: azure-cli silently returns no token on Windows.

(Verified on a Windows box: shutil.which("az")...\wbin\az.CMD, i.e. the runnable target is the .CMD shim, which the bare name never reaches.)

Fix

Resolve the executable with shutil.which("az") (which honors PATHEXT) before invoking it — exactly the fix the maintainer already applied in integrations/base.py for the same CreateProcess/.cmd problem:

az = shutil.which("az") or "az"
result = subprocess.run([az, "account", "get-access-token", ...], ...)

or "az" preserves the previous behaviour (and the existing not-installed OSError path) when az is absent. On POSIX shutil.which returns the same executable, so behaviour is unchanged there.

Verification

  • New test_resolve_token_azure_cli_resolves_executable: patches shutil.which → a fake az.CMD path and asserts the resolved path is argv[0]. Fails before (no shutil import to resolve with), passes after.
  • New test_resolve_token_azure_cli_falls_back_to_bare_az: shutil.whichNone still yields argv[0] == "az" (fallback preserved).
  • Existing azure-cli success/failure/not-installed tests unchanged: 11 passed. ruff clean.

AI-assisted: authored with Claude Code. I confirmed on Windows that shutil.which("az") resolves to az.CMD and mirrored the maintainer's existing integrations/base.py resolution.

…ndows

AzureDevOpsAuth._acquire_via_az_cli runs subprocess.run with a bare "az".
On Windows the Azure CLI is installed as az.cmd, and subprocess.run calls
CreateProcess, which does not consult PATHEXT -- so a bare "az" fails with
WinError 2 even after `az login`, and azure-cli token acquisition silently
returns None (the OSError is swallowed).

Resolve the executable with shutil.which("az") (which honors PATHEXT) before
the call, mirroring the maintainer's own fix in integrations/base.py for the
same CreateProcess/.cmd issue. `or "az"` preserves prior behavior (and the
existing not-installed OSError path) when az is absent. POSIX is unaffected.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Resolves Azure CLI authentication on Windows by locating the executable shim before invocation.

Changes:

  • Resolves az through shutil.which, retaining the existing fallback.
  • Adds tests for resolved and fallback executable paths.
Show a summary per file
File Description
src/specify_cli/authentication/azure_devops.py Resolves the Azure CLI executable before token acquisition.
tests/test_authentication.py Tests executable resolution and fallback behavior.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Medium

…ookup

Self-review catch on my own change: resolving with a bare
`shutil.which("az") or "az"` widened an execution surface. On Windows
shutil.which prepends the CURRENT DIRECTORY to the search path (unless
NoDefaultCurrentDirectoryInExePath is set) AND honors PATHEXT, so a stray
.\az.cmd / .\az.bat in the working directory resolves ahead of the real Azure
CLI -- for a credential operation. Verified: with the real az scrubbed from
PATH, shutil.which("az") returns '.\az.CMD'.

Accept the resolution only when it is absolute; otherwise fall back to the bare
"az" (which also preserves the existing not-installed OSError path). A
legitimate install always resolves absolutely, so the Windows .cmd fix this PR
exists for is unaffected. The not-installed and PATHEXT tests are extended with
relative-result cases, all of which fail before this commit.

Note: integrations/base.py resolves executables the same way; hardening that
shared path is a separate concern and is left untouched here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants