You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(ado): resolve az binary via shutil.which for Windows az.cmd (closes#1430) (#1432)
* fix(ado): resolve az binary via shutil.which for Windows az.cmd (closes#1430)
On Windows the Azure CLI ships as az.cmd (a batch wrapper). Python's
subprocess.run -> CreateProcessW does NOT honor PATHEXT for non-.exe
executables, so a bare "az" token cannot be resolved and raises
FileNotFoundError. shutil.which("az") DOES honor PATHEXT and finds
az.cmd, so AzureCliBearerProvider.is_available() returned True while
every actual subprocess call failed. The error then cascaded through
the ADO --update preflight, killing Git Credential Manager along the
way, and surfaced as the misleading 'az present but not logged in'
diagnostic -- even when the user WAS logged in via az login.
Layer 1 (root cause): resolve the az executable once in __init__ via
shutil.which and store the absolute path. Use it in every subprocess
call (_run_get_access_token, get_current_tenant_id). is_available()
now mirrors the same construction-time resolution so it can no longer
disagree with get_bearer_token()'s pre-check. Explicit absolute /
separator-bearing paths from callers (tests) are trusted verbatim.
Layer 2 (defense in depth): pipeline.py _preflight_auth_check now
strips GIT_CONFIG_GLOBAL / GIT_CONFIG_NOSYSTEM / GIT_ASKPASS from the
probe env for ADO hosts too (previously only generic). This lets Git
Credential Manager answer for Entra-cached ADO credentials when az
bearer acquisition is unavailable for any reason -- the #1430 PATH
quirk, sandbox, proxy, etc. The actual clone path remains isolated
(it builds its own clean env via GitAuthEnvBuilder.setup_environment),
so the security rationale for empty global gitconfig during clone is
preserved. Only the single ls-remote probe leg gains GCM access.
Tests:
- New TestWindowsAzCmdResolution class with the exact regression
shape: shutil.which returns a Windows az.cmd path; verify subprocess
receives it as argv[0] for both get_bearer_token and tenant probe.
- New is_available stability test (resolution is one-shot).
- Init absolute-path bypass test (preserves test injection contract).
- Existing test_ado_host_retains_credential_blocking_env renamed and
inverted to assert the new contract: ADO preflight no longer kills
GCM. Existing ADO bearer-fallback E2E unchanged (the fake-git path
has no GCM installed so behavior is unaffected).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(ado): address panel review (#1430 round 2)
Three nits surfaced by auth-expert + supply-chain-security panel:
1. Tighten the explicit-path bypass in AzureCliBearerProvider.__init__
from 'isabs OR contains os.sep' to 'isabs only'. The os.sep heuristic
accepted relative-with-separator tokens like 'subdir/az' verbatim,
handing a CWD-relative path to subprocess.run without going through
shutil.which. Only absolute paths are now trusted verbatim; everything
else flows through resolution. New test
test_init_relative_with_separator_still_resolves locks the contract.
2. Add an explicit early-return in get_current_tenant_id when
_az_command is None. Previously this case relied on the broad
'except Exception' to swallow a TypeError from passing None as
argv[0]. The new guard mirrors get_bearer_token's is_available()
pre-check. New test
test_get_current_tenant_id_returns_none_without_subprocess_when_az_missing
asserts subprocess.run is never invoked in that path.
3. Add an explicit FileNotFoundError regression-trap test
(test_bare_az_would_raise_filenotfound_but_resolved_path_succeeds)
that simulates the exact Windows CreateProcessW behaviour: argv[0]
== 'az' raises FileNotFoundError, argv[0] == resolved .cmd path
succeeds. Pins both halves of the #1430 contract in a single test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(changelog): add #1430 entry under Unreleased > Fixed
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
13
14
14
### Fixed
15
15
16
+
-`apm update` against private Azure DevOps deps no longer fails on Windows with a misleading "az present but not logged in" diagnostic when the user IS signed in via `az login`. Root cause: Python's `subprocess.run(["az", ...])` -> `CreateProcessW` does not honor `PATHEXT` for non-`.exe` executables, so the Windows `az.cmd` wrapper could not be invoked even though `shutil.which("az")` resolved it. `AzureCliBearerProvider` now resolves the `az` binary via `shutil.which` once at construction and passes the absolute path to every subprocess call. As a defense-in-depth measure, the ADO `--update` preflight probe no longer strips `GIT_CONFIG_GLOBAL` / `GIT_CONFIG_NOSYSTEM` / `GIT_ASKPASS`, so Git Credential Manager can answer for Entra-cached ADO credentials whenever bearer acquisition is unavailable for any reason (sandbox, proxy, future PATH quirks). The actual clone path keeps its full gitconfig isolation. (#1430)
17
+
16
18
- Root `.apm` hooks no longer duplicate after renaming the project directory or using git worktrees; Claude, Codex, Cursor, Gemini, and Windsurf hook configs stay idempotent across checkouts. The hook source-id is now derived from `apm.yml`'s `name` field instead of `install_path.name`, and `apm install` silently heals stale same-content entries from prior checkout basenames. Copilot is unaffected (its hooks live in per-file namespaces under `.github/hooks/`, not a shared merged config). (#1392, closes #1329)
0 commit comments