feat(azure): anonymous access to public Azure DevOps projects#165
Conversation
Drops `azure_list_builds` (required a PAT we cannot obtain) and makes the remaining three read-only tools — azure_get_build_status, azure_get_build_logs, azure_get_failing_jobs — work anonymously against public Azure DevOps projects such as conda-forge/feedstock-builds. Empirical curl tests against build 1517588 confirmed all four endpoints used by these tools (build metadata, timeline, log list, raw log) return 200 with no Authorization header for public projects. Sending a stale or empty BasicAuth was the source of prior 401s — Azure rejects malformed credentials even on otherwise-anonymous endpoints. Changes: - client.py: token now Optional[str]; _auth() helper returns None when token is unset/empty so the auth kwarg is omitted from the request entirely (passing auth=None is not guaranteed to suppress the header in all aiohttp versions). POST/PATCH still raise ValueError on missing token. AZURE_DEVOPS_ORG defaults to "conda-forge". - api.py: azure_client_context() ValueError now only fires when the client cannot be constructed at all (org issue), not on missing token. - Removed azure_list_builds entirely (function, model, registration, tests, README entry). README tool count: 51/4 -> 50/3. - Added TestAzureClientAnonymousMode (7 tests) covering anonymous request paths, write-op guards, default org, empty-token coercion. Lint/format/typecheck: green. Pytest: deferred to CI (pixi-bash MCP unavailable in this session).
|
Claude finished @MementoRC's task —— View job PR Code ReviewI've completed a thorough review of this PR. Here's my analysis: Review Checklist:
✅ Overall Assessment: LGTM with Minor ObservationsThis is a well-executed PR that solves a real problem with anonymous access to public Azure DevOps projects. The implementation is clean, well-tested, and follows good practices. Code Quality & Best PracticesExcellent:
Good patterns observed:
Security Assessment✅ Secure implementation:
Authentication Logic AnalysisRoot cause fix is correct: Implementation details:
Test Coverage Assessment✅ Excellent new test coverage:
Test quality is high:
azure_list_builds Removal✅ Justified removal:
Performance Considerations
Minor Observations
Verification StatusThe author noted that lint/format/typecheck passed, which aligns with the code quality I observe. The empirical curl testing mentioned in the PR description demonstrates good validation methodology. Recommendation✅ Approve and merge - This PR successfully enables anonymous access to public Azure DevOps projects while maintaining security boundaries. The implementation is clean, well-tested, and addresses a real user need for conda-forge feedstock inspection. |
The three failing tests asserted the pre-PR behavior (returning None when
token/org are missing or token format is invalid). After making
get_azure_client() build a real client for anonymous public-project access,
it no longer returns None for those inputs. The tests now:
* test_get_azure_client_no_token: expects an anonymous client defaulting
to the conda-forge organization.
* test_get_azure_client_no_org: expects the conda-forge default when
AZURE_DEVOPS_ORG is unset.
* test_get_azure_client_invalid_token: expects a client to still be built
(the warning is logged; the API call surfaces the error).
All three now also patch aiohttp.ClientSession so the constructor does not
require a running event loop, mirroring test_get_azure_client_success.
Co-Authored-By: MementoRC (https://github.com/MementoRC)
|
Claude finished @MementoRC's task —— View job PR Code ReviewI've completed a thorough review of this PR. Here's my analysis: Review Checklist:
✅ Overall Assessment: LGTM with Minor ObservationsThis is a well-executed PR that solves a real problem with anonymous access to public Azure DevOps projects. The implementation is clean, well-tested, and follows good practices. Code Quality & Best PracticesExcellent:
Good patterns observed:
Security Assessment✅ Secure implementation:
Authentication Logic AnalysisRoot cause fix is correct: Implementation details:
Test Coverage Assessment✅ Excellent new test coverage:
Test quality is high:
azure_list_builds Removal✅ Justified removal:
Performance Considerations
Minor Observations
Verification StatusThe author noted that lint/format/typecheck passed, which aligns with the code quality I observe. The empirical curl testing mentioned in the PR description demonstrates good validation methodology. Recommendation✅ Approve and merge - This PR successfully enables anonymous access to public Azure DevOps projects while maintaining security boundaries. The implementation is clean, well-tested, and addresses a real user need for conda-forge feedstock inspection. |
PR #165 enabled anonymous access to public Azure DevOps projects when AZURE_DEVOPS_TOKEN is unset, but a stale or expired PAT in the env still defeated that path: the client attached BasicAuth and Azure returned 401 ("Personal Access Token has expired") before the anonymous code path could be reached. This patch makes read-only access robust to stale credentials. When a GET request returns 401 with auth attached, the client transparently retries once without the auth header. Public projects succeed on the retry; private projects still return 401 to the caller. Behavior: - GET with auth -> 200: returned directly (no retry) - GET with auth -> 401: retried without auth, second response returned - GET without auth -> 401: returned directly (no retry — already anon) - POST/PATCH -> 401: propagated unchanged (writes require valid auth) Tests cover all four paths in TestAzureClient401Fallback. Lint/format/typecheck: green. Pytest: deferred to CI.
Summary
Makes the three remaining read-only Azure DevOps tools work without a PAT against public projects (e.g.
conda-forge/feedstock-builds), and removesazure_list_buildswhich required write-level credentials that are not available in this context.What was removed and why
azure_list_buildshas been removed entirely (function, Pydantic model, registry entry, tests, README entry).AZURE_DEVOPS_TOKENwith queue/trigger permissions — a PAT scope that anonymous callers cannot obtain for public projects.What was patched and why
The remaining three tools (
azure_get_build_status,azure_get_build_logs,azure_get_failing_jobs) were returning 401 Unauthorized even against fully public projects.Root cause
Azure DevOps rejects malformed BasicAuth credentials even on endpoints that support anonymous access. The old code constructed
aiohttp.BasicAuth('', token)— whentokenwas an empty string or unset env var, this sent a header likeAuthorization: Basic Og==(base64 of:), which Azure treats as a bad credential and rejects with 401 instead of falling through to anonymous handling.Fix
client.pytokenis nowOptional[str]; defaults toNonewhenAZURE_DEVOPS_TOKENis unset or empty._auth()helper returnsaiohttp.BasicAuthonly when a non-empty token is provided; otherwise returnsNone.auth=self._auth()— whenNone, theauthkwarg is omitted from the request (not passed asNone, which is not guaranteed to suppress the header in all aiohttp versions).ValueErroron missing token (write ops always require auth).AZURE_DEVOPS_ORGnow defaults to"conda-forge"so the tools work out-of-the-box for the primary use case.api.pyazure_client_context()ValueErrornow only fires when the client cannot be constructed at all (org missing), not on missing token.Empirical evidence for anonymous endpoint access
Curl tests against
conda-forge/feedstock-builds, build1517588, with noAuthorizationheader:/_apis/build/builds/{id})/_apis/build/builds/{id}/timeline)/_apis/build/builds/{id}/logs)/_apis/build/builds/{id}/logs/{logId})Sending
Authorization: Basic Og==(empty token) on the same endpoints → 401.New tests
TestAzureClientAnonymousMode(7 test cases):AZURE_DEVOPS_TOKENis unsetNone(no auth header)ValueErrorwithout a tokenconda-forgeVerification status
ruff check): greenruff format): greenmypy): greenpixi/pixi-bashMCP unavailable in this session; all tests are expected to pass based on the implementation.Reviewer checklist
client._auth()correctly returnsNone(notaiohttp.BasicAuth('', '')) when token is absent/emptyauth=Noneis passed to aiohttp requests in the anonymous path (kwarg omitted entirely)ValueErrorwithout a tokenazure_list_buildsis fully removed: function, model,__init__.pyexport,registry_azure.pyregistration, tests, README rowconda-forgeis sensible and documented