Cover GitHub URL composition with tests, and make release-commit ordering a precondition - #310
Merged
rdeago merged 2 commits intoAug 1, 2026
Conversation
The missing separator fixed in 6bba582 was possible because no single place owned the contract: the repository URL was built in the constructor without a trailing slash, and two unrelated methods appended path segments to it. The two ends could drift, and did, for three releases. GitHubRepositoryUrls now owns both ends. It takes the host, owner and name, builds the repository URL, and derives the release and file URLs from it; GitHubServerAdapter holds one and delegates. It needs no service provider, no Git origin and no token, so it is directly testable - which is why the methods it replaces never were. Two fixes fell out of writing the tests: Guard.IsTrue takes the parameter name as its second argument, not a message, so both guards were reporting 'Parameter "A path must be relative to be converted to a file URL." must be true, was false'. They now pass nameof(path) and the message to the three-argument overload. The escape check only rejected a leading "..", but Uri collapses parent segments as it parses, so enough of them anywhere in the path walked out of the repository and even out of the owner: "docs/../../../../../../etc/passwd" resolved to "https://github.com/etc/passwd". Any ".." segment is now rejected, while a name merely starting with two dots is still accepted. No changelog entry: every type involved is internal, the only caller passes a literal "CHANGELOG.md", and the URLs these methods produce were already covered by the entry for 6bba582. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AddPostReleaseCommit called EnsureReleaseCommit for its caller, which meant it could move the Git height - and with it the version - in the middle of a call whose message the caller had already formatted from that same version. That is exactly how the self-reference commit came to announce 2.1.2-preview while the release it sat on top of was tagged 2.1.3-preview. _version.Update() is called in exactly one place in the codebase, inside EnsureReleaseCommit, so the version can only move at that single moment. Requiring the release commit up front therefore removes the hazard outright, rather than working around it with a deferred message factory: once the commit exists, nothing a caller invokes can change the version underneath it. UpdateRepository keeps creating the commit implicitly, and the asymmetry is deliberate: it amends the release commit, so it owns it, and it builds its message internally once the version has settled. A post-release commit merely sits on top of that commit, so it depends on it. No functional change today - ReleaseCommand already settles the release commit before packing, which is what made that call legal in the first place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Member
Author
|
@claude please review this PR. |
|
Claude finished @rdeago's task in 2m 26s —— View job ReviewTasks
This is a tightly-scoped, well-documented fix. I didn't find any correctness issues. Details below.
|
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.
Follow-up to the two fixes pushed straight to
mainafter the 2.1.2-preview release went out tagged as2.1.3-preview. Those stopped the bleeding; this closes the two holes that let it happen.Give GitHub URL composition an owner, and tests
The missing separator fixed in 6bba582 (
.../Buildvanareleases/tag/1.1.10) was possible because nothing owned the contract: the repository URL was built in the adapter's constructor without a trailing slash, and two unrelated methods appended path segments to it. The two ends could drift, and did, for three releases.GitHubRepositoryUrlsnow owns both ends — it takes the host, owner and name, builds the repository URL, and derives the release and file URLs from it.GitHubServerAdapterholds one and delegates. It needs no service provider, no Git origin and no token, which is precisely why the methods it replaces had no tests; there are now 22 cases covering both regressions, the enterprise-host case, normalization and the guards.Two further bugs fell out of writing those tests:
Guard.IsTruetakes the parameter name as its second argument, not a message, so both guards were reportingParameter "A path must be relative to be converted to a file URL." must be true, was false. They now use the three-argument overload..., butUricollapses parent segments as it parses, so enough of them anywhere in the path walked out of the repository and even out of the owner:docs/../../../../../../etc/passwdresolved tohttps://github.com/etc/passwd. Any..segment is now rejected, while a name merely starting with two dots is still accepted.The second one is only reachable from Buildvana's own code, which passes a literal
CHANGELOG.md, so it was never exploitable — but it is exactly the class of mistake the guard exists to catch.Require the release commit before a post-release commit
AddPostReleaseCommitcalledEnsureReleaseCommiton its caller's behalf, so it could move the Git height — and with it the version — in the middle of a call whose message the caller had already formatted from that same version. That is how the self-reference commit came to announce2.1.2-previewwhile the release it sat on was tagged2.1.3-preview._version.Update()is called in exactly one place in the codebase, insideEnsureReleaseCommit, so the version can only move at that single moment. Requiring the release commit up front removes the hazard outright rather than working around it with a deferred message factory: once the commit exists, nothing a caller invokes can change the version underneath it.UpdateRepositorykeeps creating the commit implicitly, and the asymmetry is deliberate: it amends the release commit, so it owns it, and builds its message internally once the version has settled. A post-release commit merely sits on top of that commit, so it depends on it.No functional change today —
ReleaseCommandalready settles the release commit before packing, which is what made that call legal in the first place.Notes
No changelog entry: every type involved is internal, and the user-visible URLs were already covered by the entry for 6bba582.
Verified with
dotnet bv pack(314 tests, 0 warnings) andinspectcode --severity=WARNING(0 results).