Skip to content

Wire existing bv options to buildvana.json (Config Phase 2)#290

Merged
rdeago merged 6 commits into
Tenacom:mainfrom
rdeago:feature/269-config-phase-2
Jun 1, 2026
Merged

Wire existing bv options to buildvana.json (Config Phase 2)#290
rdeago merged 6 commits into
Tenacom:mainfrom
rdeago:feature/269-config-phase-2

Conversation

@rdeago
Copy link
Copy Markdown
Member

@rdeago rdeago commented Jun 1, 2026

Closes #269. Config Phase 2 (sub-issue of #267): makes the *Settings injectables config-aware so each option resolves flag → buildvana.json → default, and replaces mainBranch with release.generateDocsFrom. The config model/loader/schema from Phase 1 (#268) are unchanged — this PR only consumes them.

What changed

  • DotNetSettings / DotNetInvocationSettings (new): resolve dotnet.configuration plus the per-command sub-objects (all, restore, build, test, pack, nugetPush), each carrying args + env.
  • DotNetService: injects DotNetSettings; each invocation passes its own sub-object to RunDotNetAsync, which merges dotnet.all then the per-command settings. Argument order is base → dotnet.all → per-command → forwarded command-line args (so a -- argument still wins); environment variables apply dotnet.all then per-command. bv-internal probes pass no sub-object, so they receive no user-configured args/env.
  • BuildPipeline: takes its default build configuration from DotNetSettings.Configuration instead of a hardcoded "Release".
  • ReleaseSettings: config-aware resolution — configuration chain (--configurationrelease.configurationdotnet.configuration), release.checkPublicApi, release.dogfood, release.changelogUpdates + release.emptyChangelog, and release.generateDocsFrom.
  • ChangelogService.PrepareForRelease: substitutes release.emptyChangelog text into an empty "Unreleased changes" section; an empty section with no configured substitute fails the release (in ReleaseCommand, which owns the policy).
  • mainBranch removed end-to-end: deleted GitService.MainBranch/FindMainBranch, the --main-branch global flag and its pre-parsing. Documentation generation is now gated by matching the current short branch name against the release.generateDocsFrom regexes (default ["^main$", "^master$"]); the changelog permalink in generated release notes now points at the current branch.

Breaking changes

  • --main-branch removed (docs gating now via release.generateDocsFrom).
  • --unstable-changelog / --require-changelog removed with no CLI replacement (changelog policy is repo-stable: release.changelogUpdates + release.emptyChangelog).

CHANGELOG updated accordingly (amended the in-cycle, still-unreleased entries that referenced the now-removed flags rather than appending contradictory bullets).

Testing

  • dotnet build Buildvana.slnx — clean (0 warnings).
  • Buildvana.Tool.Tests — 52/52 pass (added coverage for the configuration chain, release config layering, and generateDocsFrom matching).
  • ReSharper inspectcode --severity=WARNING — 0 results.

🤖 Generated with Claude Code

Make the *Settings injectables config-aware so each option resolves
flag -> buildvana.json -> default:

- Add DotNetSettings/DotNetInvocationSettings, resolving dotnet.configuration
  and the per-command (all/restore/build/test/pack/nugetPush) args and env.
  DotNetService merges dotnet.all with the per-command settings, appending
  forwarded command-line arguments last; BuildPipeline takes its default
  configuration from DotNetSettings instead of a hardcoded "Release".
- Make ReleaseSettings config-aware: configuration chain
  (--configuration -> release.configuration -> dotnet.configuration),
  release.checkPublicApi, release.dogfood, release.changelogUpdates +
  release.emptyChangelog (config-only, replacing the removed
  --unstable-changelog/--require-changelog flags), and release.generateDocsFrom.
- ChangelogService.PrepareForRelease substitutes release.emptyChangelog text
  for an empty "Unreleased changes" section; an empty section with no
  substitute fails the release.
- Replace mainBranch with release.generateDocsFrom: remove GitService.MainBranch
  and FindMainBranch, the --main-branch global flag and its pre-parsing; gate
  documentation generation by matching the current branch against the configured
  regular expressions, and point the changelog permalink at the current branch.

Closes Tenacom#269

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added 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 Jun 1, 2026
@rdeago rdeago added enhancement [issue/PR] requests / implements new or improved functionality. refactor [issue/PR] requests / implements a refactor of existing code without affecting functionality. labels Jun 1, 2026
@rdeago
Copy link
Copy Markdown
Member Author

rdeago commented Jun 1, 2026

@claude please review this PR.

@claude
Copy link
Copy Markdown

claude Bot commented Jun 1, 2026

Claude finished @rdeago's task in 4m 27s —— View job


PR Review

  • Read changed files
  • Analyze settings resolution chains and logic
  • Check for correctness issues
  • Verify test coverage
  • Post review findings

Overall the PR is clean and well-structured. The three-tier resolution chain (flag → config → default) is applied consistently, and the removal of mainBranch/FindMainBranch in favour of release.generateDocsFrom regexes is a solid upgrade. No correctness bugs found. A few observations worth considering:


ContinuousIntegrationBuildArg position change (behaviour shift)

src/Buildvana.Tool/Services/DotNetService.cs

In the old code, forwardedArgs were spread into the base args array before ContinuousIntegrationBuildArg, so the CI flag came last and was authoritative — a user's -- -p:ContinuousIntegrationBuild=false was silently overridden.

In the new code the CI arg is part of args (the base), and commandLineArgs (forwarded) are appended by RunDotNetAsync after it. As the PR states, "forwarded args always win." So now bv build -- -p:ContinuousIntegrationBuild=false would actually override the server-injected value.

This is consistent with the new design, but it is a subtle behavioural change from before. Worth a conscious decision rather than just a side-effect of the refactor. If the intent is to make the CI flag immutable on cloud builds, it should remain last (or move to after commandLineArgs); if forwarded-wins is the intended policy everywhere, the current placement is correct. Either way, a comment on the property or a test asserting the ordering would make the intent explicit.


Resolve allocates a Dictionary when config.Env is null

src/Buildvana.Tool/Services/DotNetSettings.cs, line 62

: new(config.Args ?? [], config.Env ?? new Dictionary<string, string?>());

When config is non-null but Env is absent, a fresh Dictionary<string, string?> is allocated rather than reusing ReadOnlyDictionary<string, string?>.Empty (the same static instance already used in DotNetInvocationSettings.Empty). The BuildEnvironment null-return fast-path (all.Count == 0 && perCommand.Count == 0) is unaffected, so there is no functional issue — this is a trivial allocation nit. Swapping to ReadOnlyDictionary<string, string?>.Empty would make the empty-settings path fully allocation-free. Fix this →


dotnet.all is merged into nuget push invocations

src/Buildvana.Tool/Services/DotNetService.cs, line 247 / RunDotNetAsync

configuredArgs = [.. _settings.All.Args, .. invocation.Args] applies to every non-internal RunDotNetAsync call, including nuget push. If a user places MSBuild flags such as -p:Foo=Bar or -m:1 in dotnet.all.args, they will be forwarded to dotnet nuget push, which ignores unknown flags silently. This is probably harmless in practice, but it is worth documenting in the schema / buildvana.json docs so users know dotnet.all truly means all dotnet invocations.


MatchesDocsBranch guard pattern

src/Buildvana.Tool/Subcommands/ReleaseSettings.cs, lines 157–160

Guard.IsNotNull(branch);
if (string.IsNullOrEmpty(branch))
{
    return false;
}

Guard.IsNotNull throws on null, making the null arm of IsNullOrEmpty unreachable. The two guards are not wrong, but slightly inconsistent — either Guard.IsNotNullOrEmpty (and remove the if) or keep the graceful empty-return and drop the Guard in favour of only the if. The current shape is fine since ReleaseCommand already validates !string.IsNullOrEmpty(git.CurrentBranch) before reaching this, but cleaning it up would clarify intent.


Test coverage of arg/env merging

BuildEnvironment and the configuredArgs construction in RunDotNetAsync are not unit-tested directly. The logic is simple enough that the risk is low, but a couple of focused tests for the "all + per-command merge" and "null invocation skips configured args" cases would catch any future regression here cheaply.


Everything else looks correct

  • Resolution chains (--flag → release.X → dotnet.X → default) in ReleaseSettings are correct and well-tested.
  • MatchesDocsBranch correctly returns false on empty branch and handles RegexMatchTimeoutException.
  • GenerateDocsFrom: [] (explicit empty list) correctly disables docs on every branch (no fallback to defaults), which is the right semantic for an explicit override.
  • The emptyChangelogSubstitute flow in ReleaseCommand + PrepareForRelease is consistent: HasUnreleasedChanges() and CollectNewSectionLines() apply the same empty-section semantics; the double-check in PrepareForRelease is redundant but safe.
  • Probe calls (InvocationKind.Internal) correctly receive no user-configured args.
  • Changelog URL change from _git.MainBranch to _git.CurrentBranch is intentional and matches the stated design.
  • DotNetInvocationSettings as a record is fine for this use; it's never compared by value.

rdeago and others added 5 commits June 1, 2026 09:03
An invalid configuration file will prevent execution of _any_ `bv`
subcommand, even those that do not depend on configuration.
This has been flagged as a behavioral change during review. It is a
deliberate decision and stays, but a clarification was due.
Replace the InvocationKind enum (Normal/Informational/Internal) with
direct, mechanical parameters on RunDotNetAsync, so each call site
spells out exactly how it controls the invocation:

- args + tiers (IReadOnlyList<DotNetInvocationSettings>, folded left to
  right) + commandLineArgs + trailingArgs, applied in that order.
  trailingArgs win over everything, so ContinuousIntegrationBuild moves
  there and stays unoverridable by forwarded/configured arguments.
- appendVerbosity and an OutputStreaming struct (Disabled / Unconditional
  / AtVerbosity) replace the streamOutput/streamVerbosity pair, making the
  illegal "disabled but with a verbosity" state unrepresentable.
- The internal MSBuild probe passes empty tiers instead of relying on a
  null sentinel; BuildEnvironment folds into RunDotNetAsync.

Remove the now-stale partial on DotNetService where only the enum needed
it, then re-add it for the OutputStreaming nested struct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pull the tier-folding loop out of the private RunDotNetAsync into an
internal static MergeInvocation method, and add unit tests covering the
arg ordering and environment layering (tier override, null-value
removal, and the no-tiers/no-env cases).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@rdeago
Copy link
Copy Markdown
Member Author

rdeago commented Jun 1, 2026

Thanks for the review @claude!

ContinuousIntegrationBuildArg position change (behaviour shift)

This is a deliberate decision, now enforced even more strictly. The ContinuousIntegrationBuild MSBuild property shall not be user-overridable.

Resolve allocates a Dictionary when config.Env is null

Fixed.

dotnet.all is merged into nuget push invocations

This is a deliberate decision and will need documenting as soon as we have actual documentation. For now, we'll count on "all" being pretty self-explanatory.

MatchesDocsBranch guard pattern

Fixed. Now Guard guards against nulls and the if condition only checks for an empty branch.

Test coverage of arg/env merging

Following a refactor that happened in this PR, the elements you mentioned are gone. Tests have been added respecting the spirit of your request though: argument ordering and environment layering are properly covered.

Other, unrelated changes have also been added. Please review again.

@claude
Copy link
Copy Markdown

claude Bot commented Jun 1, 2026

Claude finished @rdeago's task in 2m 36s —— View job


PR Review (Round 2)

  • Read changed files
  • Verify ContinuousIntegrationBuildArg "enforced more strictly" change
  • Verify Resolve Dictionary → ReadOnlyDictionary.Empty fix
  • Verify MatchesDocsBranch Guard pattern fix
  • Review test coverage for arg/env merging
  • Review other changes (OutputStreaming extraction, config annotation, changelog, BuildPipeline, GitHubServerAdapter)
  • Post review findings

All previous concerns have been addressed correctly. Here's what I found on each point, plus a look at the new additions:


Previous review — resolution status

ContinuousIntegrationBuildArg now in trailingArgs
ContinuousIntegrationBuildArg is now passed via trailingArgs at every call site (RestoreSolutionAsync, BuildSolutionAsync, TestSolutionAsync, PackSolutionAsync). The XML doc on RunDotNetAsync makes the contract explicit: "trailing args cannot be overridden by configured or command-line arguments (e.g. the ContinuousIntegrationBuild property)". Clean.

Resolve allocation fix
DotNetSettings.cs line 63 now correctly uses ReadOnlyDictionary<string, string?>.Empty when config.Env is null.

MatchesDocsBranch guard pattern
Guard.IsNotNull(branch) handles the null case, if (branch.Length == 0) return false; handles the empty case. No redundancy, clear intent.

Test coverage for arg/env merging
DotNetServiceMergeInvocationTests covers exactly the right paths:

  • Argument ordering (base → tiers → command-line → trailing)
  • Later tier overrides earlier tier for the same env key
  • Null env value preserved as a removal marker
  • Empty tiers: [] produces no configured args and leaves environment null (the bv-internal probe path)
  • Tiers with args but no env variables also leave environment null

New changes

DotNetService.OutputStreaming.cs
Clean extraction of the OutputStreaming struct from the (now-deleted) DotNetService.InvocationKind.cs into its own partial-class file. The three factories (Disabled, Unconditional, AtVerbosity) are clear; AtVerbosity taking an argument is correctly a method rather than a property.

DotNetInvocationConfig.cs
Adding [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] is the right fix for ReSharper warning suppression on JSON-deserialized properties. XML doc added consistently.

BuildPipeline.cs
DefaultConfiguration constant removed; configuration parameters are now nullable and resolve via configuration ?? _dotnetSettings.Configuration in RunAsync. The resolution is in the right place — a single point rather than scattered at call sites.

GitHubServerAdapter.cs
Changelog permalink now uses _git.CurrentBranch instead of _git.MainBranch. Consistent with the stated design.

CHANGELOG.md
Phase 1 bullet rewritten to reflect that buildvana.json is no longer inert. New Phase 2 entries cover the dotnet.* arg/env settings and the release.* settings, including the breaking removals of --unstable-changelog/--require-changelog and --main-branch. The env-var removal list is trimmed to match only the vars that were actually removed. All consistent with the PR.


Nothing significant to flag

The code is correct and consistent throughout. LGTM — ready to merge.

@rdeago rdeago merged commit 6cdd706 into Tenacom:main Jun 1, 2026
5 of 6 checks passed
@rdeago rdeago deleted the feature/269-config-phase-2 branch June 1, 2026 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:code [issue/PR] affects project code (excluding tests). area:docs [issue/PR] affects documentation (excluding XML documentation that is part of source code). enhancement [issue/PR] requests / implements new or improved functionality. refactor [issue/PR] requests / implements a refactor of existing code without affecting functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Config Phase 2 — wire existing bv options to buildvana.json

1 participant