Stop reporting the version from a constructor - #349
Conversation
VersioningService was a computed snapshot wearing a service's name: it did all its work in its constructor, VersionService.Update() threw the instance away and built another, and ComputeVersion copied its ten properties into an identically shaped record. It also reported the version from that constructor, so the message came out once per construction — once per MSBuild node, since the SDK's cache is per process. The count tracked MSBuild's node topology and nothing a reader cares about: four lines on a 4-vCPU runner, twenty-nine on a 32-core desktop. Split it into the pair the sibling GitHeightCalculator/GitHeightResult already establishes: VersionInfo carries the snapshot and derives the version strings, VersionCalculator does the lookups and returns one. Calculating twice across a commit is now the explicit way to get a second answer, and IReporter leaves Buildvana.Core.Versioning entirely. The SDK task keeps reporting the version it computes, at detail level, where the per-node repetition is a diagnostic rather than noise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Until now the only place bv stated the version it was releasing was the VersioningService constructor, which said it whenever an instance happened to be built — at startup, then again after the version bump — rather than because anyone had decided to. With that gone, say it once, on purpose, right after the final consistency check: everything that could still move the version has happened by then, so the line names the version that actually gets published. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@claude please review this PR. |
|
Claude finished @rdeago's task in 5m 1s —— View job Review of #349 — Stop reporting the version from a constructor
Static review only — I did not build here (self-hosting needs the preview feed); I'm relying on the gate you already ran. The split is right
Findings1. The regression test does not test for the regression — The bug was not "the version is never stated", it was "the version is stated twenty-two times". await Assert.That(harness.Notices).Contains($"Releasing version {harness.ComputeVersion()}.");
await Assert.That(harness.Notices.Count(x => x.StartsWith("Releasing version", StringComparison.Ordinal))).IsEqualTo(1);The 2.
Not a live bug — var tagMatchesSpec = spec.Prerelease == !string.IsNullOrEmpty(prereleaseTag);
Guard.IsTrue(tagMatchesSpec, nameof(prereleaseTag));Your call: it is defensive code on a path nothing takes today. 3. A release that fails before the artifact pass no longer names its version — The notice sits after 4. Nit — the line precedes the last thing that can still refuse the release —
5. Nit —
Nothing here blocks: findings 3–5 are judgment calls, 2 is latent, and only 1 is something I'd want in before merge. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #349 +/- ##
==========================================
+ Coverage 74.63% 74.65% +0.02%
==========================================
Files 164 165 +1
Lines 4596 4589 -7
Branches 788 788
==========================================
- Hits 3430 3426 -4
+ Misses 1015 1014 -1
+ Partials 151 149 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The bug this branch fixes was not a missing version line, it was the same version line twenty-two times, and `Contains` passes identically for one occurrence and for twenty-two. Assert the count, so that a relapse fails the test that is named after the behaviour. The SDK-side tests already count, which is why that side would have caught a relapse and this one would not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ComputeInformationalVersion` chose between `.` and `-` on `spec.Prerelease`, but the question it is really asking is whether the string it appends to already carries a prerelease part, and that is decided two lines earlier by `prereleaseTag`. The two agree for every version the calculator produces, so this fixes nothing today; it removes the possibility of them disagreeing. Had they disagreed, the output would have been a version string that is not a semantic version at all: a spec marked prerelease with no tag yielded `2.3.1.gdeadbeef01`, a dot-separated identifier appended where there was no prerelease part to append it to. Wrap the declaration while here: at 125 characters it was over the 120 allowed for a declaration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`VersionCalculator.Calculate` is the only thing that builds a `VersionInfo`, and it lives in the same assembly, so the constructor has no reason to be public. Narrowing it means the one place that has to get the parameters consistent with each other is also the only place that can pass them. The type stays public: it is what `Calculate` returns, and the SDK task and the tool both read its properties. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The notice sat immediately above the check that refuses a version whose tag already exists, so a release that stopped there announced `Releasing version 2.3.1.` and then refused to release it. Moving the line below the check makes the claim in its comment true: after this point nothing left can reject this particular version, and a later failure is a build failure rather than a verdict on the version. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`GetOrComputeVersion` handed back a `CachedVersion`, so the caller had to reach through `.Version` for the only thing it wanted, and a type that exists to hold a cache fingerprint appeared in the task body. Returning `VersionInfo` keeps `CachedVersion` where it belongs: entirely inside the file that caches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing in the tree ran the version calculation against a repository that has no HEAD, which left the one thing the task does with a missing commit ID - turning it into the empty string, because MSBuild properties have no null - untested, and showed up as the single partial branch in the coverage report. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three lines ran past 140 characters, two of them lines this branch changed. The repeated `engine.Messages.Count(...Contains(...))` becomes a named helper, which is what made those two long in the first place, and the configuration document moves into a raw literal held by a local - as an argument it would span multiple lines and trip SA1118. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Addressed. Seven commits; the gate ( 1 — the regression test now tests the regression. await Assert.That(harness.Notices.Count(x => x.StartsWith("Releasing version", StringComparison.Ordinal))).IsEqualTo(1);2 — fixed, though not with the guard. The invalid output is real, but a guard throws on a state that a smaller change makes unrepresentable. @rdeago's point closes it from the other side: the constructor is now One correction to the premise, since it is what made narrowing free: 3 — no code change; now stated in the description. The observation is right, but what was lost was a guess rather than information. 4 — taken. 5 — taken. Two more, neither of them yours:
@claude, another look when you have a moment. |
|
Claude finished @rdeago's task in 2m 45s —— View job Re-review of #349 — seven follow-up commits
All five findings are addressed. Nothing new blocks; one micro-nit at the end, take it or leave it. Verification, finding by finding1 — 2 — Both separator branches are covered, as it happens: The Your correction on the premise is right, and it does change the calculus: no 3 — description, no code. Agreed, and the point about 4 — 5 — The two you added
One nit
/// Gets the informational version: <see cref="SemVer"/>, plus a <c>g</c>-prefixed short commit ID
/// appended to the prerelease part — becoming it, when the version has none — when the build is not
/// a public release.Pure prose; nothing depends on it. Ready to merge from my side. |
The summary of InformationalVersion said the commit ID is appended to the prerelease part, which is true of a prerelease and not of a stable version, where the ID becomes that part. That distinction is the whole point of deriving the separator from the prerelease tag, so the doc was the one place left stating it loosely. Also name the other way a plain SemVer comes back: a repository with no commit has no ID to append. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Description
bv releaseprinted the same version line up to twenty-two times per run — four in a row at the head of each pipeline stage, plus two frombvitself.The cause is that
VersioningServicereported the computed version from its constructor. That made the message count a function of how many instances got built, which in the SDK means one per MSBuild node, since theComputeVersioncache is per process. Four lines on a 4-vCPU CI runner; twenty-nine on a 32-core desktop. Nothing about the repository determines it.The class had a second problem behind the first: it was named a service but behaved as a snapshot. It computed everything in its constructor,
VersionService.Update()discarded the instance and built another, and the SDK task copied its ten properties into an identically shaped record.So it is now the pair its own project already establishes with
GitHeightCalculator→GitHeightResult:VersionInfo— the immutable snapshot, deriving the five version strings from the spec, the height, and the settings. No I/O, and aninternalconstructor:VersionCalculatoris the only thing that builds one, so the one place that has to get the parameters consistent is the only place that can pass them.VersionCalculator— the lookups (version file, prerelease tag validation, height walk), returning aVersionInfo. Stateless, so calculating again after a commit is the explicit way to get a second answer.IReporteris out ofBuildvana.Core.Versioningaltogether.CachedVersioncollapses to(Fingerprint, VersionInfo)and stays inside the caching partial. Reporting now happens in exactly two deliberate places:MessageImportance.Low), invisible below-v detailed, where per-node repetition is a diagnostic rather than noise — and whereComputeVersionTestsreads it as evidence of a cache hit;bv release, once, just after the tag check — the last thing that can refuse this particular version — where naming it records a decision rather than a guess.bv build/packsay nothing about the version, as before.One consequence worth stating plainly: a release that fails before that point no longer names a version at the default verbosity. What it used to name was a guess —
VersionServiceis resolved at the top of the command, before the version spec change and before the release commit, so the old line reported a height at least one below the version that would be published, and named a different version line entirely on a--bumprun.-v detailedstill carries the SDK task's line for anyone who wants the provisional number.Additional changes
None.
Checklist
bv pack+ ReSharper at WARNING and above, both silent)Release_RecordsTheVersionItPublishes, which asserts the line appears exactly once