Repository navigation
Replace Octokit with GitHubApiClient over HttpClient - #407
Merged
Merged
Conversation
GitHubServerAdapter reaches the GitHub REST API through Octokit, which fixes the API at api.github.com, so a release on GitHub Enterprise Server cannot reach its API. GitHubApiClient takes the API base URL from the host name of the repository, and sends every request through an HttpMessageHandler that a test can replace. An asset upload goes to the upload_url of its release, as in Octokit. It carries its label in the query string, which saves a second request. When no tag matches a name exactly, git/refs/tags/<name> answers with every tag whose name starts with it, so TagExistsAsync asks for git/ref/tags/<name>. StubHttpMessageHandler answers by method and URL, and records the headers and the body of each request, so that GitHubApiClientTests checks what GitHubApiClient sends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ServerRelease.DisposeAsync disposed GitService, which the service provider owns and disposes again at the end of the run. Every consumer held a GitService, whose type exposes Dispose, so nothing told a borrowed service from an owned one. IGitService does not extend IDisposable, and the service provider registers GitService under it. A consumer that is itself disposable then holds no disposable field, and CA2213 has nothing to report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHubServerAdapter reached the GitHub REST API through an Octokit GitHubClient, whose base address is always api.github.com. A release on GitHub Enterprise Server then went to the wrong API. GitHubServerAdapter now holds one GitHubApiClient for its whole life, over an HttpClientHandler with automatic decompression. The service provider disposes the adapter, and the adapter disposes the client. The upload carries the label of the asset, so UploadReleaseAssetAsync sends no second request to set it. DeleteReleaseAsync asks TagExistsAsync whether the tag exists, where it caught the NotFoundException of Octokit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReSharper reported five warnings in GitHubApiClient.cs, GitHubApiClientTests.cs, and StubHttpRequest.cs. GitHubApiClient.SendAsync set HttpRequestMessage.Content in the initializer of a using declaration. A throwing setter would then leak the request. No test read StubHttpRequest.Method. TagExistsAsync_KeepsTheSlashesOfATagName asserts it, because the fallback answer of StubHttpMessageHandler matches any method. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The <value> element of GitHubServerAdapter.IsCloudBuild said that the property always returns false. The property returns true, as on every CI adapter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`docs/tool-commands/release.md` did not say which GitHub API `bv release` reaches. `GitHubApiClient` derives the address from the host of the `origin` remote. Before a first release on a GitHub Enterprise Server, a user needs to know the derived address. The `CHANGELOG.md` bullet records the move from `api.github.com` alone to the API of the host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #407 +/- ##
==========================================
+ Coverage 82.97% 83.37% +0.39%
==========================================
Files 284 285 +1
Lines 8359 8473 +114
Branches 1645 1654 +9
==========================================
+ Hits 6936 7064 +128
+ Misses 1146 1132 -14
Partials 277 277 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
rdeago
commented
Oct 21, 2026
rdeago
left a comment
Member
Author
There was a problem hiding this comment.
Review by Claude. Self-review.
One finding. It comes from the failed codecov/patch check, on the added file GitHubApiClient.cs.
GitHubApiClient.ReadErrorMessageAsync and DotNetReleaseIndex.ReadJsonAsync returned from inside an await using block, within a try block. Control then never reached the closing brace of the try block, and the coverage report counted that brace as a line no test runs. Both methods now assign the result inside the await using block and return after the try block, as GitHubApiClient.ReadAsync does. A code review named ReadErrorMessageAsync alone, and a search of src/ found the same shape in ReadJsonAsync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHubApiClient.Describe read the URL of a failed request from HttpRequestMessage.RequestUri, which is nullable. Every request has a URL, so the null branch of the ?. operator could never run, and the coverage report counted it as a partial line. GitHubApiClient.ApiUrl now builds the absolute URL of each request, and SendCoreAsync, EnsureSuccessAsync and ReadAsync receive it alongside the request. ApiUrl combines BaseUrl with the path as HttpClient combined BaseAddress with a relative request URL. The requests go to the same addresses, and HttpClient.BaseAddress is no longer set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rdeago
commented
Oct 21, 2026
rdeago
left a comment
Member
Author
There was a problem hiding this comment.
Review by Claude. Self-review.
👍 Approved.
No blocking findings. The two fixes for the finding of the previous round hold at the head commit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Checklist of related issues / discussions
Proposed changes
GitHubServerAdapterreached the GitHub REST API through Octokit, whose base address is alwayshttps://api.github.com. A release on a GitHub Enterprise Server could not reach its own API. The other hosts that #402 adds have no Octokit either.GitHubApiClient, insrc/Buildvana.Tool/Services/ServerAdapters/Internal/GitHub/, replaces Octokit. It is a small client overHttpClient, with the operationsGitHubServerAdaptercalls:IsPrivateRepositoryAsync;The API base URL follows the host of the
originremote. Forgithub.com, it ishttps://api.github.com. Any other host is taken for a GitHub Enterprise Server, athttps://<host>/api/v3.Every request carries
Accept: application/vnd.github+json,X-GitHub-Api-Version: 2022-11-28,Authorization: Bearer <token>, and aUser-Agentwith the name and version ofbv. The request and response models are records, serialized through the source-generatedGitHubJsonContext. An answer outside the 2xx range fails the build with the status code and themessageof the body. A 404 on the tag lookup means that the tag does not exist.GitHubServerAdapterholds oneGitHubApiClientfor its whole life. The service provider disposes the adapter, and the adapter disposes the client.Where the code differs from the issue
https://uploads.github.comforgithub.com,https://<host>/api/uploadsotherwise.GitHubApiClientuses theupload_urlthat GitHub states in the answer to the creation of the release, as Octokit'sUploadAssetdoes. The server then names its own upload address, and the client derives nothing it cannot measure.labelin the query string, next toname. Octokit set the label with a second request,PATCH .../releases/assets/{id}, which is gone.TagExistsAsyncasks forgit/ref/tags/<tag>, singular. Octokit asked forgit/refs/tags/<tag>. When no tag matches exactly, that endpoint answers with every tag whose name starts with<tag>, so a missing tag could look present. The delete stays ongit/refs/tags/<tag>, the only endpoint that deletes a reference.X-GitHub-Api-Versionstays2022-11-28instead of the more recent2026-03-10, for compatibility with GitHub Enterprise Server 3.19 and 3.20. The remarks ofGitHubApiClient.ApiVersionhold the reason and the possible version switch dates.Tests
StubHttpMessageHandleranswers by method and URL, with a status code, and records the method, the headers, and the body of each request.GitHubApiClientTestscovers every operation: the URL, the method, the headers, the body sent, and the value read from the answer. The error path covers a refusal of every operation, an answer without a message, an unreadable answer, an unreachable host, a timeout, and cancellation.Risks
GitHubApiClientsends is invisible to a unit test. The first release after the merge will be the final test. A failure in the asset upload or in the publication of the release comes after the NuGet push. The rollback ofbv releasethen removes the commits, the tag, and the GitHub release, but the packages stay on the feed.Additional changes
IGitService.ServerRelease.DisposeAsyncdisposedGitService, a singleton that the service provider owns and disposes again at the end of the run. Every consumer held aGitService, so nothing told a borrowed service from an owned one.IGitServicedoes not extendIDisposable, the service provider registersGitServiceunder it, and every consumer depends on it.ServerReleaseno longer disposes it.GitHubServerAdapter.IsCloudBuild. Its<value>documentation said that the property always returnsfalse. The property returnstrue, and the documentation says so.DotNetReleaseIndex.ReadJsonAsync. It returned from inside anawait usingblock, within atryblock. The coverage report then counted the closing brace of thetryblock as a line that no test runs. The method now returns after thetryblock, as the methods ofGitHubApiClientdo.Types of changes
This pull request introduces the following types of changes:
.gitattributes,.gitignore)Breaking changes
This pull request introduces breaking changes:
Checklist
🤖 Generated with Claude Code