Repository navigation
Publish releases on GitLab - #410
Merged
Merged
Conversation
GitHubApiClient held the code that sends a request, turns an error answer into a BuildFailedException, and reads a JSON answer. A GitLab client and a Gitea client need the same code. HostApiClient holds it, and each client supplies its base URL, its headers, and the reader of its error answers. The requests and the diagnostics of GitHubApiClient do not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The line was 143 characters, over the limit of 140. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HostApiClient put the credentials in the default headers of its HttpClient, so no request could go without them. A GitLab job token cannot read a project, so bv must read it without credentials. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bv had no client of the GitLab REST API. An access token goes in PRIVATE-TOKEN, because the generic package registry warns against Authorization: Bearer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buildvana.jsonc could not name GitLab as the host of a repository, nor the variable that holds a GitLab token. GitLabConfig mirrors GitHubConfig, with GITLAB_TOKEN as the default of TokenEnv. Nothing reads hosts.gitlab yet, and GitHost.Create still treats HostKind.GitLab as an unknown host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitLabApiClient could not read the access level of a token on a project. The permissions member of GET /projects/:id misses a membership through a shared group, so GetProjectMemberAsync reads members/all, which states the level GitLab enforces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHubRepositoryUrls.File held the only check that keeps a file URL inside the repository, so a second host would have copied it. RepositoryFilePath.ToUrlPath holds it for every host that builds file URLs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHost.Create made an UnknownHost for every GitLab repository, so bv release created no release page there. GitLab has no draft releases, so GitLabReleasePublisher uploads every ReleaseAsset to a generic package named after the repository, then creates a release that links them. A rollback deletes that package, which needs the Maintainer role, so BeginAsync refuses an access token below it before the release commit. GitLab refuses some file names and duplicate link names, so PublishAsync checks every ReleaseAsset with CheckAssets before the first upload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitLabReleasePublisher.CheckAssets ran in PublishAsync, after the NuGet push, so a refused asset left its packages on the feed. ServerRelease.PushUpdates calls ReleasePublisher.CheckAssets instead, because nothing has left the clone before the push. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The documentation said that bv knows no GitLab host. Publishing in release.md is split by Git host, because GitLab adds rules on tokens, roles, and file names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHub names an asset after its file, and refuses a second upload of the same name. `GitHubReleasePublisher` found out at that upload, after the tag and the packages were pushed, so the release rolled back. `GitHubReleasePublisher.CheckAssets` now refuses the list before the push, and `ReleasePublisher.Duplicates` serves both hosts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
terminology.md names it "Git host". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hosts table of `docs/introduction.md` listed GitHub Enterprise as supported with no limit. `Microsoft.SourceLink.GitHub.props` declares `github.com` alone, so SourceLink on a GitHub Enterprise Server needs a `SourceLinkGitHubHost` item. The table gives each self-hosted server its own row, so that the limit stays off the rows of `github.com` and `gitlab.com`. No release has run on a GitHub Enterprise Server, so its row says "Untested", as the self-managed GitLab row does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHub Enterprise Cloud with data residency has different URL patterns from GitHub Enterprise Server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since 37457ab, bv release takes a .ghe.com host for GitHub Enterprise Cloud with data residency. release.md still gave every host but github.com the API path of a GitHub Enterprise Server. The hosts table in introduction.md names the ContentUrl of the SourceLinkGitHubHost item. SourceLink reads raw content from https://<host>/raw by default, and GHE.com serves it from https://raw.<host>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Supported, but untested" is different from "untested, and we don't even try to tell it apart". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReSharper reported AccessToDisposedClosure on harness.ArtifactsPath in HookBehavior lambdas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #410 +/- ##
==========================================
+ Coverage 86.41% 86.82% +0.41%
==========================================
Files 299 307 +8
Lines 8744 9055 +311
Branches 1707 1749 +42
==========================================
+ Hits 7556 7862 +306
- Misses 904 905 +1
- Partials 284 288 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
rdeago
commented
Oct 25, 2026
rdeago
left a comment
Member
Author
There was a problem hiding this comment.
Review by Claude. Self-review.
One blocking finding, in the rollback of a GitLab release. The comment on GitLabReleasePublisher.UndoAsync describes it.
A failed release deletion stopped GitLabReleasePublisher.UndoAsync before DeletePackageAsync, so the package stayed. The rollback deletes the release tag before UndoAsync runs, and GitLab deletes the release with its tag, so DeleteReleaseAsync can get a 404. It now returns false on a 404, and any other failure is a warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rdeago
commented
Oct 25, 2026
rdeago
left a comment
Member
Author
There was a problem hiding this comment.
Review by Claude. Self-review.
👍 Approved.
No blocking findings.
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
GitHost.Createmade anUnknownHostfor every GitLab repository, sobv releasecreated no release page on GitLab. This PR adds GitLab as a Git host, ongitlab.comand on a self-managed instance.GitLabApiClientreaches the REST API athttps://<host>/api/v4, and addresses the project by its URL-encoded path.GitLabHostbuilds the release and file URLs, and decides the token kind and the push username.GitLabReleasePublisherchecks the token, uploads the assets to the generic package registry, creates the release, and undoes both on a rollback.buildvana.jsoncacceptsgitlabinrepository.host, andhosts.gitlab.tokenEnv,GITLAB_TOKENby default.GitHost.DetectKindtakesgitlab.comfor GitLab on every CI platform. On GitLab CI, it also takes the host ofCI_SERVER_URLfor GitLab, throughGitLabCIPlatform.ServerKind.The token works as the issue says:
tokenEnvisCI_JOB_TOKEN, the token is the job token of GitLab CI. The API requests carryJOB-TOKEN, and the push username isgitlab-ci-token. The comparison is case-sensitive.oauth2.The release:
GitLabReleasePublisher.BeginAsyncreadsGET /projects/:id/releases?per_page=1as the permission probe.PublishAsyncuploads each asset withPUT /projects/:id/packages/generic/<package>/<tag>/<file name>?select=package_file. The package is named after the repository, the last segment ofrepository.url. Its version is the release tag.PublishAsyncthen creates the release, with one link of typepackageper asset, anddirect_asset_pathset to/<file name>. The name of a link is the description of the asset.CHANGELOG.mdon the release branch.UndoAsyncdeletes the release, then the package, each when it exists.release.mdhas a "Publishing on GitLab" section. It states the token kinds, the Maintainer role, the push setting a job token needs, the generic package, and the file name rules.environment-variables.mddescribesCI_SERVER_URLandhosts.gitlab.tokenEnv. The hosts table ofintroduction.mdhas a row for GitLab and one for a self-managed GitLab.Where the code differs from the issue
PRIVATE-TOKEN, where the issue saysAuthorization: Bearer. The documentation of the generic package registry listsPRIVATE-TOKEN,JOB-TOKEN,DEPLOY-TOKEN, and Basic, and warns against any other method.BeginAsyncreads the releases of the project, where the issue says it reads the project. A job token reaches the Releases, Release links, and Packages APIs, and does not reach the Projects API.BeginAsyncreadsGET /user, thenGET /projects/:id/members/all/:user_id. It refuses anaccess_levelbelow 40 before the release commit.permissionsofGET /projects/:idmisses access through a shared group.members/allreports the effective access level.bvchecks none.IsPrivateRepositoryAsyncwith a job token. A job token cannot read the project, soGitLabHostreads it without credentials. GitLab answers 200 for a public project and 404 otherwise, and 404 counts as private. No production code calls the member.direct_asset_path.GitLabReleasePublisher.CheckAssetsrefuses such assets before the first commit leaves the clone.ServerRelease.PushUpdatescalls the virtualReleasePublisher.CheckAssetsbefore the push, andReleaseCommandcollects the assets before it. The asset collection moves before the push on every host, as step 15 ofrelease.mdsays.ReleaseCommandGitLabTestsruns a release throughGitLabHostoverStubHttpMessageHandler, asReleaseCommandGitHubTestsdoes for GitHub.GitLabReleasePublisherTestscovers the role check, the asset check, and the rollback.Risks
release.mdsays so.Additional changes
HostApiClient.GitHubApiClientheld the code that sends a request, reads a JSON answer, and turns an error answer into aBuildFailedException.HostApiClientholds it now, forGitLabApiClientand for the Gitea client of Publish releases on Gitea and Forgejo, and run on Gitea Actions #406. The requests and the diagnostics ofGitHubApiClientdo not change. A request carries the credentials only when the client asks for them, so thatGitLabHostcan read a project without them.RepositoryFilePath.GitHubRepositoryUrls.Fileheld the check that keeps a file URL inside the repository.RepositoryFilePath.ToUrlPathholds it for every host.GitHubReleasePublisherfound out at that upload, after the push of the tag and the packages.GitHubReleasePublisher.CheckAssetsrefuses the list before the push. The changelog has a bullet under "Changes to existing features".bv releasetook a host ending in.ghe.comfor a GitHub Enterprise Server, with the API athttps://<host>/api/v3.GitHubApiClient.GetBaseUrlreturnshttps://api.<host>/for such a host, andGitHost.DetectKindtakes it for GitHub on every CI platform.release.mddescribes it, and the changelog has a bullet under "New features".introduction.md. The table has one row per Git host, so that the rows ofgithub.comandgitlab.comshow no limit. The rows of GitHub Enterprise Server, GHE.com, and self-managed GitLab state the SourceLink item each needs.Microsoft.SourceLink.GitHub.propsandMicrosoft.SourceLink.GitLab.propsdeclaregithub.comandgitlab.comalone. A GHE.com host serves raw content fromhttps://raw.<host>, where SourceLink defaults tohttps://<host>/raw.introduction.md. "Untested" becomes "Supported (untested)" for a Git host or a CI platformbvsupports, and "??? (untested)" elsewhere.introduction.md, and one sentence ofgetting-started.md, say "Git host", asterminology.mdnames it.GitHubApiClient.GetRepositoryAsync. Its arguments are wrapped, because the line was 143 characters.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