Skip to content

Split ServerAdapter into a CI platform, a Git host, and a release publisher - #409

Merged
rdeago merged 12 commits into
Tenacom:mainfrom
rdeago:404-platforms-and-hosts
Oct 23, 2026
Merged

rdeago merged 12 commits into
Tenacom:mainfrom
rdeago:404-platforms-and-hosts

Conversation

@rdeago

@rdeago rdeago commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Checklist of related issues / discussions

Proposed changes

bv release ran on GitHub Actions alone, pushed to the remote that HEAD tracks, and left the release tag to GitHub. ServerAdapter held three things at once: the CI runner, the Git host of the repository, and the release on that host. GitHubServerAdapter also opened the Git repository in its constructor, so bv build failed on GitHub Actions in a home directory without an origin remote.

Three types replace ServerAdapter, as #402 designs them:

  • CIPlatform, in Services/Platforms/, reads the environment and nothing else. GitHubActionsPlatform, GitLabCIPlatform, GenericCIPlatform, and LocalPlatform implement it.
  • GitHost, in Services/Hosts/, describes the Git host of repository.url. GitHubHost and UnknownHost implement it.
  • ReleasePublisher creates the release on the host. GitHubReleasePublisher works over the GitHubApiClient of bv release talks to GitHub over HttpClient, and Octokit goes #403. UnknownHostReleasePublisher creates nothing.

The behavior of bv changes as follows:

  • bv release runs on any machine. It needs repository.url, a remote of the clone that names that repository, a committer identity, and the token of the host.
  • UrlMatchingReleaseRemoteResolver finds the release remote. It compares the host name and the path segments, case-insensitive, and ignores the scheme, the port, the user, and a .git suffix.
  • bv release fetches the tags of the release remote before it checks the versions.
  • ServerRelease creates the release tag at the release commit and pushes it before publication. On a rollback, it deletes the tag, locally and on the remote. GitService.PushTag refuses to move a remote tag, as Git does.
  • On an unknown host, bv release fails unless release.allowUnknownHost is true. With the setting, it tags and pushes without a release page.
  • The build pipeline depends on CIPlatform alone, and no longer opens the Git repository. It passes ContinuousIntegrationBuild=true on every CI platform, and the pipeline run of bv release passes it on every platform.
  • ChangelogService handles a host without a release URL. The section title gets no link, a relative file link stays as it is, and each case writes a notice.
  • bv info prints the CI platform, the Git host, the release remote, the token variable, and the committer identity. ReleaseCommitterIdentity resolves the identity for both bv info and bv release.
  • buildvana.jsonc gets a repository section and release.allowUnknownHost. hosts.github replaces the github section.

buildvana.next.jsonc of this repository states repository.url and hosts.github. The pinned bv reads neither. The bv that this PR releases reads the file that the post-release hook copies over.

Where the code differs from the issue

  • Generic CI detection. GenericCIPlatform recognizes a CI runner as the .NET CLI does, where the issue says "CI is set and not false". The variables and the values that read as true come from CIEnvironmentDetectorForTelemetry and EnvironmentVariableParser.ParseBool in dotnet/sdk. The changelog bullet links environment-variables.md#variables-that-mark-a-ci-runner.
  • No breaking change for github. The github section never reached a stable release. The unreleased bullet that introduced github.tokenEnv names hosts.github.tokenEnv instead.
  • HostKind. It holds Auto and GitHub alone. Publish releases on GitLab #405 and Publish releases on Gitea and Forgejo, and run on Gitea Actions #406 add their values.
  • Test fakes. The fake of CIPlatform is FakeCIPlatform, because it records nothing. RecordingHost and RecordingReleasePublisher replace the two other fakes the issue names.
  • Changelog links. The tag bullet links release.md#the-release-tag, and the fetch bullet links release.md#the-release-remote. Both sections are new on the page.

The SSH refusal stays

The issue asks to measure whether LibGit2Sharp exposes an anonymous remote. The source of LibGit2Sharp 0.32.0 says:

  • An anonymous remote exists for a fetch and for ListReferences alone. Commands.Fetch falls back to git_remote_create_anonymous when its argument names no remote.
  • No Network.Push overload takes a URL.
  • Every public Remote comes from RemoteCollection, whose Add writes the repository configuration.

bv cannot push to repository.url without adding a remote to .git/config. UrlMatchingReleaseRemoteResolver keeps refusing a matched remote with an SSH URL.

Risks

  • repository.url is a new required setting of bv release. A repository that does not state it cannot release until it does.
  • The tag push event fires before the release exists, where it used to fire at publication. A workflow that a tag push triggers runs before the release page exists.
  • The fetch of the tags is a network call that CI did not make before. It uses the credentials of the push.
  • The release flow on a real GitHub host is tested against a stubbed API. The first release after the merge is the final test.

Additional changes

  • Failed push or fetch. GitService fails when the remote rejects a push. A rejected push used to pass as a success. A push that is not a fast-forward, and any other failure of a push, a fetch, or a reference listing, used to end bv with an unhandled exception. GitService turns each one into a BuildFailedException that names the remote. The changelog has a bullet for each fix under "Bugs fixed in this release".
  • Committer identity. bv release keeps its committer identity in memory, and no longer writes user.name and user.email to .git/config. The changelog has a bullet under "Changes to existing features".
  • GITHUB_OUTPUT. GitHubActionsStepOutputWriter writes the file without a byte order mark, and writes a multi-line value in the delimiter form GitHub documents. GitHubServerAdapter appended with Encoding.UTF8, which writes a byte order mark into an empty file.
  • terminology.md. The table names the CI platform, the Git host, and the release remote, with the names they replace. Three older sentences of CHANGELOG.md and release.md called a Git host a server, and say "Git host" now.

Types of changes

This pull request introduces the following types of changes:

  • Bug fix
  • New feature
  • Test addition / update (no changes to non-test code)
  • Refactor (no changes in public API syntax or semantics)
  • Performance improvement (no changes in public API syntax or semantics)
  • Documentation update (not including changelog)
  • Dependency addition / update
  • Changes to toolchain configuration
  • Changes to CI (workflows, bot / app configurations)
  • Changes to repository files (.gitattributes, .gitignore)
  • Other

Breaking changes

This pull request introduces breaking changes:

  • Yes
  • No

Checklist

  • For all types of changes:
  • For code changes only:
    • The project builds on my machine, via the provided build script, with zero warnings
    • I have added tests that prove my feature works / my fix is effective
    • I have added / modified XML documentation according to changes in code
    • I have checked that all the links I added or modified in XML documentation point to their intended destination
  • For documentation changes only:
    • I have checked and proofread documentation locally
    • I have checked that all the links I added or modified point to their intended destination

🤖 Generated with Claude Code

rdeago and others added 7 commits October 22, 2026 11:24
buildvana.jsonc names no repository for bv release, so a release goes to whatever repository the origin remote of the clone names. repository.url and repository.host name the target repository and its kind of Git host. hosts.github replaces the top-level github section, so that each kind of host gets a section of its own. release.allowUnknownHost and --allow-unknown-host let a release proceed on a host that bv does not know. Nothing reads repository.url, repository.host, or release.allowUnknownHost yet. buildvana.jsonc drops its github section, whose value is the default, so that buildvana.next.jsonc can state hosts.github and still pass RepositoryConfigFilesTests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DotNetService took ServerAdapter, whose GitHubServerAdapter opens the Git repository in its constructor. On GitHub Actions, bv build then failed in a home directory without an origin remote. ServerAdapter.IsCloudBuild also knew GitHub Actions and GitLab CI alone. CIPlatform reads the environment and nothing else. Its GenericCIPlatform detector follows CIEnvironmentDetectorForTelemetry of the .NET CLI, so that bv and dotnet agree on what a CI runner is. GitHubActionsStepOutputWriter writes the GITHUB_OUTPUT file without a byte order mark, and writes a multi-line value in the delimiter form GitHub documents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Push always went to the remote HEAD tracks, and there was no way to create, push, or fetch a tag. Each operation now takes the remote name. A push the remote rejects now fails, where it used to pass as a success. CommitterIdentity no longer writes to .git/config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bv release ran on GitHub Actions alone, pushed to the remote HEAD tracks, and left the tag to GitHub. It now runs on any CI platform, finds the release remote from repository.url, and creates and pushes the tag itself. An unknown host releases without a release page when release.allowUnknownHost is true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before bv release, a user had no way to see which CI platform, Git host, release remote, token variable, and committer identity bv detected. A wrong guess showed up as a failed release. InfoCommand prints them, and states why a value cannot be resolved. ReleaseCommitterIdentity resolves the committer identity for both InfoCommand and ReleaseCommand, so that the two cannot disagree. InfoCommand writes to IAnsiConsole.Profile.Out.Writer, because a redirected console wraps text at 80 columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ReSharper reported UseAwaitUsing on the synchronous disposal of the ServiceProvider in InfoCommandTests.RunAsync. The test now disposes it with await using, in the form DotNetServiceResolutionTests and Program already use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A blank line separated the two breaking-change bullets about bv release from the other bullets of "Changes to existing features". GitHub then rendered the whole list with paragraph spacing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rdeago rdeago added enhancement [issue/PR] requests / implements new or improved functionality. breaking [issue/PR] requires / contains breaking changes. labels Oct 23, 2026
@github-actions github-actions Bot added area:ci [issue/PR] affects GitHub Actions. area:docs [issue/PR] affects documentation (excluding XML documentation that is part of source code). area:code [issue/PR] affects project code (excluding tests). labels Oct 23, 2026
@codecov

codecov Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.48506% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.41%. Comparing base (9762b2f) to head (a9eb577).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...ol/Services/Hosts/GitHub/GitHubReleasePublisher.cs 89.85% 2 Missing and 5 partials ⚠️
...c/Buildvana.Tool/Services/Release/ServerRelease.cs 92.59% 2 Missing and 2 partials ⚠️
...Services/Hosts/UrlMatchingReleaseRemoteResolver.cs 93.33% 0 Missing and 2 partials ⚠️
src/Buildvana.Core.Testing/TempGitRepo.cs 94.73% 0 Missing and 1 partial ⚠️
src/Buildvana.Tool/Services/Git/GitService.cs 98.98% 1 Missing ⚠️
...Buildvana.Tool/Services/Hosts/GitHub/GitHubHost.cs 96.96% 0 Missing and 1 partial ⚠️
...a.Tool/Services/Platforms/GitHubActionsPlatform.cs 95.00% 1 Missing ⚠️
...ldvana.Tool/Services/Platforms/GitLabCIPlatform.cs 90.00% 1 Missing ⚠️
...Buildvana.Tool/Services/Platforms/LocalPlatform.cs 66.66% 1 Missing ⚠️
src/Buildvana.Tool/Subcommands/ReleaseCommand.cs 97.14% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #409      +/-   ##
==========================================
+ Coverage   83.37%   86.41%   +3.04%     
==========================================
  Files         285      299      +14     
  Lines        8473     8744     +271     
  Branches     1654     1707      +53     
==========================================
+ Hits         7064     7556     +492     
+ Misses       1132      904     -228     
- Partials      277      284       +7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rdeago rdeago left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Review by Claude. Self-review.

⚠️ Changes requested.


The code has no blocking findings. Three documentation findings follow, each in a code-anchored comment.

Non-blocking

  • CHANGELOG.md: the bullet on the rejected push links a section that no longer describes the push of the commits.
  • terminology.md: "Git host" and "release remote" have no row in the table, and one changelog bullet calls the Git host a server.
  • release.md, "Rollback": the opening sentence ties the rollback to a draft release, which an unknown host never creates.

Comment thread CHANGELOG.md Outdated
Comment thread .claude/rules/project/terminology.md
Comment thread docs/tool-commands/release.md
rdeago and others added 3 commits October 23, 2026 09:19
The changelog bullet on a rejected push linked "Publishing" in release.md, which describes the NuGet push alone. The repository.host row of configuration-file.md linked the same section, which does not describe the host detection. The links now point at "The release tag" and "Preconditions", where those facts are stated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two CHANGELOG.md bullets and "The release commit" of release.md called a Git host a server. The other pages call it a Git host. terminology.md had no row for the Git host or the release remote, so nothing stated which name to use. The two rows now state both names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Rollback" said that bv release undoes its steps when one fails after the draft release exists. On an unknown host there is no draft, yet ServerRelease.BeginAsync registers every rollback action all the same. A reader on such a host would expect a failed release to leave its commits and its tag in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@rdeago rdeago left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Review by Claude. Self-review.

👍 Approved.


No blocking findings. The three findings of the first round are fixed at the head commit, and the commits that fix them introduce no new problem.

rdeago and others added 2 commits October 23, 2026 10:14
No test reached the OnPushStatusError handler of GitService.Push.
PushBranch_WhenNotFastForward_Throws passes without the handler, because
LibGit2Sharp refuses a non-fast-forward push before the remote sees it.
TempGitRepo.LockRemoteReference creates the lock file of a reference in
a remote that TempGitRepo.AddBareRemote created. The remote then fails
to update the reference, and reports it through OnPushStatusError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a push was not a fast-forward, LibGit2Sharp threw
NonFastForwardException before the remote saw the push. Program catches
only BuildFailedException, so bv release ended with a stack trace. Any
other LibGit2SharpException of GitService.Push, ListRemoteReferences, or
FetchTags did the same, such as a failed authentication or an
unreachable remote. GitService wraps each one in a BuildFailedException
that names the remote, and keeps the original as the inner exception.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@rdeago rdeago left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Review by Claude. Self-review.

👍 Approved.


No blocking findings. The two commits since the last round, 4cb4c3a and a9eb577, introduce no new problem.

@rdeago
rdeago merged commit 23c4b84 into Tenacom:main Oct 23, 2026
7 checks passed
@rdeago
rdeago deleted the 404-platforms-and-hosts branch October 23, 2026 09:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:ci [issue/PR] affects GitHub Actions. area:code [issue/PR] affects project code (excluding tests). area:docs [issue/PR] affects documentation (excluding XML documentation that is part of source code). breaking [issue/PR] requires / contains breaking changes. enhancement [issue/PR] requests / implements new or improved functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Split ServerAdapter into a CI platform, a Git host, and a release publisher

1 participant