Fix the release workflow's hook failure, and the home-directory bug behind it - #348
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hooks are file-based apps living under .buildvana, so home-directory discovery runs from there on every hook build. A configuration file in that directory satisfied the marker rule's clause for a configuration file sitting directly in the directory, so each hook discovered .buildvana as its own home directory: the repository's stylecop.json was rejected as external to the repository, leaving hooks to compile under StyleCop's defaults, and every other HomeDirectory-relative path was wrong by one level. The location was a nice-to-have for repositories grouping Buildvana files away from the root, and it shipped in one preview. Removing it makes the collision unrepresentable rather than guarding against it: every marker now sits in the directory it marks, so nothing under a subdirectory takes part in discovery, and the rule reads the same in the loader, in the canonical discovery implementation, and in Sdk.props. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The static loader found the file on every call, so a run could hold two answers to which file it was reading: the DI factory found it and threw the path away, bv update found it again to rewrite its schema reference, and the run-time information passed to hooks carried no path at all. BuildvanaConfigLoader becomes BuildvanaConfigProvider, an instance holding both facts and resolving each on first read, the way HomeDirectoryProvider resolves the home directory. Finding the file is now the provider's own business: the probe is private, so no caller can ask a second time and get a different answer. bv registers the provider and sources the parsed configuration from it, leaving every consumer of the data alone; the SDK task hands the provider the home-directory provider it was already building. LoadFile survives as a public static for the one caller that must bypass the cache: bv update re-reads the file it has just rewritten, and wants a parse that postdates the rewrite rather than the one the provider may already hold. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A hook had no way to ask which file bv read: the args carried the well-known directories but not the configuration file, so a hook working on that file had to name it, and one naming the wrong file failed at run time, mid-release. RuntimeInfo gains ConfigFile, the absolute path of the file this run read. It is a required member, so a run cannot omit the fact, and a nullable one, because a repository whose home directory is marked by Git alone legitimately has no configuration file at all. HookArgs.LoadConfig() reads that same file through the new BuildvanaConfig.LoadFile, so a hook after settings does not search either: which file to read comes from the args, what it says is read from disk at the moment of the call, and an earlier hook's rewrite is therefore visible rather than shadowed by a snapshot. The contract-evolution section is rewritten around what actually constrains this contract. A hook is compiled from source at every run against the Runtime version the SDK pins, which bv refuses to mismatch, and its args file is rewritten immediately before it runs: writer and reader are the same version by construction, and the JSON crosses no version boundary. What has to stay stable is the source surface a hook compiles against, which is why an addition may be required. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hook read buildvana.jsonc by name, relative to its working directory, which is why it crashed when the file moved: the name and the location were its own guesses rather than facts of the run. It now asks BuildvanaConfig.FindFile, and does nothing when the repository has no configuration file. RuntimeInfo.ConfigFile would be the better source, and a comment says so, together with what to replace and why it has to wait: the SDK pins Buildvana.Runtime to its own version, so a hook compiles against the last published release, and that member ships with the next one. Using it now would fail the hook's compilation during the very release that publishes it, aborting the release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two settings had a built-in default that lived in whoever read them: bv resolved dotnet.configuration to Release, and the versioning code resolved versioning.assemblyVersionPrecision to Major. A hook asking the same questions of the same configuration got null for both, so the same file described two different builds depending on who read it. Each default moves next to the setting it belongs to, as a constant, with an Effective... accessor over it. The accessor extends the nullable section type, because an absent section says exactly what an absent setting says: with the receiver nullable, config.DotNet.EffectiveConfiguration answers in both cases and no consumer writes a fallback of its own to get wrong. bv, SDK tasks, and hooks now resolve a setting through one definition rather than agreeing to spell the same fallback the same way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@claude please review this PR. |
|
Claude finished @rdeago's task in 7m 30s —— View job ReviewRead the full diff against The diagnosis is right and the fix is the right shape: collapsing the marker rule to every marker sits in the directory it marks makes the collision unrepresentable instead of guarding against it, and it's mirrored consistently in all four places (loader, Findings below, correctness first. 1.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #348 +/- ##
==========================================
+ Coverage 74.57% 74.63% +0.05%
==========================================
Files 162 164 +2
Lines 4582 4596 +14
Branches 788 788
==========================================
+ Hits 3417 3430 +13
- Misses 1014 1015 +1
Partials 151 151 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The remarks told a settings-reading hook it had no use for the member and sent it to BuildvanaConfig.Load, which searches. LoadConfig, added in the same commit, is LoadFile(RuntimeInfo.ConfigFile), and Hooks.md tells hooks to call it precisely so they do not search, so the member documented itself as irrelevant to its own principal consumer. This is the copy a hook author reads in IntelliSense while deciding, so it now names LoadConfig as the way settings-readers reach the file, and keeps the rest: the member itself is for hooks that work on the file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tests renamed with this branch all pass with the .buildvana/ probes put back, so nothing failed if the location returned. That matters most for Sdk.props, which mirrors the C# algorithm by discipline alone and where a restored probe is silent. What the removed clause actually did was make a directory a home directory because a .buildvana subdirectory of it held a configuration file, so that is the fixture: .git/HEAD at the root, a configuration file in root/nested/.buildvana/, discovery starting at root/nested. The old rule answered nested, the current one answers root. One test per implementation, plus FindFile returning null for a file left in .buildvana/, which is the probe itself. Each was checked against the old behavior by restoring the probes: one failure per test project, each of them the new test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ConfigFile is required and nullable, so System.Text.Json wants it present in the document, and it is there only because nothing tells the serializer to drop nulls. A JsonIgnore(WhenWritingNull) on the member, or the same default on the context, would make every hook run in a repository without a configuration file fail to load its args, at release time. The round-trip test only ever covered a run that had one, since the sample args always named a file. It now runs both ways, off one optional parameter that also spares LoadConfig_WithoutConfigFile_ReturnsEmptyConfig its two-level with-expression. Checked by attributing the member: one failure, the null case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The interim search called FindFile with no argument, which resolves against the current directory. That is the home directory, because bv runs hooks from there, but it makes the hook depend on process state to reach a fact its own args already carry. Passing RuntimeInfo.HomeDirectory says the same thing without the assumption, and leaves one expression to replace once RuntimeInfo.ConfigFile is published, which is what the comment above it promises. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four entries added by this branch landed where they were written rather than where they belong: two constants between BuildvanaConfig's own two, and an extension accessor between LoadFile and the equality operators. The file is read as a diff at every API review, and it is sorted everywhere else. The whole file now matches its sorted order line for line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moving the defaults into the configuration model left the settings classes documenting them by value: VersioningSettings named AssemblyVersionPrecision .Major, DotNetSettings said "Release". Those are the last copies that can drift from the constants, which is the thing that commit set out to remove. Both now point at the constant, so a changed default reads correctly everywhere without anyone remembering to look. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The paragraph on rewriting the configuration file introduced the member in its JSON spelling, then showed C# using it. The table above it, and the code block below it, both say RuntimeInfo.ConfigFile; the camelCase form belongs where the document describes the args file's own syntax. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Addressed, one commit per finding. Gate ( 1.
|
| fixture | old rule | current rule |
|---|---|---|
.git/HEAD at root, config at root/nested/.buildvana/buildvana.jsonc, start at root/nested |
root/nested |
root |
One test per implementation on that fixture (HomeDirectoryDiscoveryTests, SdkPropsTests), plus FindFile(dir) returning null for a file left in .buildvana/, which is the probe itself (BuildvanaConfigLoadTests). Hermetic: the .git/HEAD marker is inside the fixture, and any stray marker above the temp directory has a shorter path, so nearest-wins keeps it out.
Each was checked against the old behavior by restoring the probes in all three implementations — HomeDirectoryDiscovery, BuildvanaConfig.FindFile, and Sdk.props: one failure per test project, each of them the new test.
Not added: the fourth test, in BuildvanaConfigProviderTests. The provider has no probe of its own — it delegates to BuildvanaConfig.FindFile — so a restored probe is caught by the Buildvana.Runtime test, and a copy there would pin nothing further.
3. Null round-trip — done (b4fd520)
Load_RoundTripsWhatBvWrites now runs both ways, off an optional parameter on SampleArgs that also spares LoadConfig_WithoutConfigFile_ReturnsEmptyConfig its two-level with expression.
Both regressions were checked:
DefaultIgnoreCondition = WhenWritingNullon the context — 3 failures, one of them the new null case. (The other two areReleaseInfo.PreviousVersion, required and nullable for the same reason.)[JsonIgnore(Condition = WhenWritingNull)]on the member — 1 failure, the null case. That is the one only this test catches.
4. Hook's interim FindFile() — verified, second half done (7faa374)
FindFile is public in 2.1.208-preview: git show 2.1.208-preview:src/Buildvana.Runtime/BuildvanaConfig-Load.cs has public static string? FindFile(string? homeDirectory = null), and the tag's PublicAPI.Unshipped.txt carries the entry. dotnet build .buildvana/hooks/release/post-release.cs compiles clean against the pin. No aborted release waiting there.
The argument point is taken: it now passes hookArgs.RuntimeInfo.HomeDirectory, so the hook depends on what bv said rather than on process state, and the deferred edit is one expression.
5. Nits — all three done
- baa2528 — the four entries are sorted; the whole file now matches its sorted order line for line, so the next diff has nothing incidental in it.
- 19e00d1 —
VersioningSettingscitesVersioningConfig.DefaultAssemblyVersionPrecision. AndDotNetSettings.cs:33, which spelled out"Release"— same drift, same commit, since that is exactly what the defaults commit set out to remove. - ca00901 —
RuntimeInfo.ConfigFilein the C# paragraph.
6. Observation
Agreed on both counts, and no change made: the alternative puts the fallback back at the call sites, which is the thing that commit removes. Noted as a convention.
Nothing here changed the PR description: all of it fixes code this PR introduced, which is not what the "Additional changes" section is for.
@claude please review again.
|
Claude finished @rdeago's task in 4m 31s —— View job Re-reviewRead the seven new commits ( All five findings are addressed, and two of them came back better than what I asked for. 2 — you're right and my fixtures were wrongI traced it. Under the collapsed rule, The delivered fixture is the right one, and I verified each of the three against the pre-fix code by hand:
Each fails on the old code, each for the reason the removed clause existed. Hermeticity holds in all three: the fixture plants The rationale for not adding the fourth test in And the 1, 3, 4, 5 — verified
One observation, no change requested
Nothing else. The delta is clean, the three new tests do the job the deleted ones stopped doing, and I have no further findings. Not done: no build or test run here, so the gate result is yours, not mine; and the |
Checklist of related issues / discussions
No issue: this fixes a failing release workflow, and went straight to a PR.
Proposed changes
The release workflow failed: the
release/post-releasehook died withFileNotFoundException: '/home/runner/work/Buildvana/Buildvana/buildvana.jsonc'after the configuration file moved into
.buildvana/. The hook read the file byname, relative to its working directory.
That literal was the presenting symptom. The cause underneath it is that
a configuration file inside
.buildvana/makes.buildvana/itself a homedirectory: the marker rule counted a configuration file "directly in the
directory", and
.buildvana/buildvana.jsoncsatisfies that clause for.buildvanaitself. Hooks are projects living in there, so every hook buildwalked up, stopped at
.buildvana/, and took it for the repository. Measuredbefore the fix:
and after:
That second line is the
SA1633warning in the same release log: the repository'sstylecop.jsonwas found and then discarded as "outside the repository", so hookscompiled under StyleCop's defaults instead of ours. Every other
HomeDirectory-relative path in a hook build was wrong by one level too. Noanalyzer rule was touched to fix it.
What changed
.buildvana/is no longer a configuration-file location. The file lives inthe home directory;
.buildvana/keeps the hooks. The marker rule collapses toone sentence with no exceptions — every marker sits in the directory it marks —
in the loader, in
HomeDirectoryDiscovery, and inSdk.props, which now probetwo candidates instead of four. Removing the location makes the collision
unrepresentable rather than guarding against it.
bvresolves the configuration file once per run.BuildvanaConfigLoaderbecomes
BuildvanaConfigProvider, an instance holding both the path and theparsed configuration, each resolved on first read, as
HomeDirectoryProviderdoes for the home directory. The probe is private, so nothing can ask a second
time and get a different answer. Previously three places found the file
independently and one of them threw the path away.
RuntimeInfo.ConfigFilecarries the path,and
HookArgs.LoadConfig()reads that same file through the newBuildvanaConfig.LoadFile, so a hook after settings does not search either —which file comes from the args, what it says is read from disk at the moment of
the call.
Known limitation, deliberate: the hook uses
BuildvanaConfig.FindFile()ratherthan
RuntimeInfo.ConfigFile, with a comment saying what to replace and when. TheSDK pins
Buildvana.Runtimeto its own version, so a hook compiles against the lastpublished release; using the new member now would fail the hook's compilation during
the very release that publishes it, aborting that release. It becomes a one-line
change in the release after next.
Additional changes
dotnet.configuration→Releaseand
versioning.assemblyVersionPrecision→Majorused to be resolved bywhoever read them, so a hook asking the same question of the same file got
nullwhere
bvgot a value — one file describing two different builds. Each default isnow a constant next to its setting, with an
Effective…accessor over a nullablereceiver, so an absent section and an absent setting resolve identically and no
consumer writes its own fallback.
binary-compatibility reasoning to a contract with no version boundary: a hook is
compiled from source at every run against the Runtime version the SDK pins, which
bvrefuses to mismatch, and the args file is rewritten immediately before therun. What has to stay stable is the source surface, which is why
ConfigFilecould be added as a required member.
Types of changes
This pull request introduces the following types of changes:
docsdirectory) update.gitattributes,.gitignore)Breaking changes
This pull request introduces breaking changes:
The
.buildvana/configuration location shipped in exactly one preview(
2.1.208-preview) and never in a stable release — its own bullet was still under## Unreleased changes— so its removal edits that bullet rather than adding abreaking-change entry. Migration for a preview adopter is
git mv.Checklist
docsdirectory) only: