Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `bv --version` prints the tool's informational version and exits without running a command and without printing the startup logo.
- `bv` root help (`bv --help`) now shows a `GLOBAL OPTIONS:` section listing the options every subcommand inherits (`--verbosity`/`-v`, `--color`, `--no-color`, `--nologo`, `--version`). These options are now position-independent (accepted before or after the subcommand name) and case-insensitive, matching the rest of `bv`'s option surface.
- Commands that forward extra arguments to `dotnet` (`restore`, `build`, `test`, `pack`) are marked as such in `bv`'s root help, and their per-command help (`bv <command> --help`) includes a `FORWARDED ARGUMENTS` section.
- Buildvana now recognizes a repository-root configuration file, `buildvana.json` (or its commented variant `buildvana.jsonc`). The file is **inert** in this release: it is discovered, parsed, validated, and exposed to `bv`, but no setting affects the build yet. A committed JSON schema (`schemas/buildvana.schema.json`) is generated from the typed model, so editors can validate and document the file; unknown keys, an invalid file, or the presence of both `buildvana.json` and `buildvana.jsonc` in the same directory are reported as errors. Both `bv` and the SDK now treat a `buildvana.json`/`buildvana.jsonc` file as a home-directory marker, alongside `.buildvana-home` and Git markers; home-directory discovery now stops at the nearest directory (the starting directory included) that contains any marker.
- Buildvana now recognizes a repository-root configuration file, `buildvana.json` (or its commented variant `buildvana.jsonc`). It is discovered, parsed, validated, and exposed to `bv`; the settings it currently drives are listed below, and more are wired in over subsequent releases. A committed JSON schema (`schemas/buildvana.schema.json`) is generated from the typed model, so editors can validate and document the file; unknown keys, an invalid file, or the presence of both `buildvana.json` and `buildvana.jsonc` in the same directory are reported as errors and will prevent `bv` from executing _any_ subcommand, even those that are not driven by any configuration setting (e.g., `clean`).
- Both `bv` and the SDK now treat a `buildvana.json`/`buildvana.jsonc` file as a home-directory marker, alongside `.buildvana-home` and Git markers; home-directory discovery now stops at the nearest directory (the starting directory included) that contains any marker.
- `buildvana.json` now drives several build and release settings that were previously CLI-only or hardcoded. Each resolves as CLI flag (where one exists) → `buildvana.json` → built-in default:
- the default build configuration (`dotnet.configuration`, default `Release`), used by `bv restore`/`build`/`test`/`pack` and as the base of `bv release`'s configuration chain (`--configuration` → `release.configuration` → `dotnet.configuration`);
- extra arguments and environment variables for each `dotnet` invocation: `dotnet.all` (applied to every invocation) merged with the per-command `dotnet.restore`/`dotnet.build`/`dotnet.test`/`dotnet.pack`/`dotnet.nugetPush`, each carrying `args` and `env`. Arguments are appended in the order base → `dotnet.all` → per-command → forwarded command-line arguments (so a `--` argument still wins); environment variables apply `dotnet.all` then the per-command entries;
- the `bv release` settings `release.checkPublicApi`, `release.dogfood`, `release.changelogUpdates` + `release.emptyChangelog`, and `release.generateDocsFrom`.

### Changes to existing features

Expand All @@ -29,19 +34,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **BREAKING CHANGE**: `bv clean` (formerly known as `bv prepare`) now deletes the `TestResults` directory at the repository root.
- `bv clean` (formerly known as `bv prepare`) no longer deletes per-project `TestResults` directories.
- `dotnet bv release` no longer folds self-reference (dogfood) updates into the "Prepare release" commit. They now go into a separate `Update self-references to <version> [skip ci]` commit pushed on top, in the same push. The release tag binds to the "Prepare release" commit, so checking out the tag and rebuilding now reproduces the actually-released source state (which still references the previously-published versions). `[skip ci]` is required on the dogfood commit because the new packages are usually not yet published at push time.
- **BREAKING CHANGE**: Five `bv release` options have been renamed:
- **BREAKING CHANGE**: Three `bv release` options have been renamed:
- `--versionSpecChange` → `--bump`
- `--checkPublicApiFiles` → `--check-public-api`
- `--updateChangelogOnPrerelease` → `--unstable-changelog`
- `--ensureChangelogNotEmpty` → `--require-changelog`
- `--updateSelfReferences` → `--dogfood`
- **BREAKING CHANGE**: `bv` no longer accepts CLI option values via environment variables. The following env vars are no longer recognized as defaults for their CLI counterparts:
- `CONFIGURATION` (`--configuration`)
- `MAIN_BRANCH` (`--main-branch`)
- `VERSION_SPEC_CHANGE` (`--versionSpecChange`, now `--bump`)
- `CHECK_PUBLIC_API_FILES` (`--checkPublicApiFiles`, now `--check-public-api`)
- `UPDATE_CHANGELOG_ON_PRERELEASE` (`--updateChangelogOnPrerelease`, now `--unstable-changelog`)
- `ENSURE_CHANGELOG_NOT_EMPTY` (`--ensureChangelogNotEmpty`, now `--require-changelog`)
- `UPDATE_SELF_REFERENCES` (`--updateSelfReferences`, now `--dogfood`)

Pass values via CLI flags instead.
Expand All @@ -60,8 +60,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `bv test -- --report-trx` reaches the test application.
- **BREAKING CHANGE**: `bv release` rejects a `--` separator (and anything after it): unlike the pipeline commands, it has no underlying `dotnet` pass-through to forward arguments to.
- **BREAKING CHANGE**: `bv` no longer forces `-maxcpucount:1` on the `dotnet` invocations of `restore`/`build`/`test`/`pack`. MSBuild now uses its default parallelism unless you forward your own `-m`/`-maxcpucount` switch.
- **BREAKING CHANGE**: The `-c`/`--configuration` option is no longer parsed by `bv restore`/`build`/`test`/`pack`; for those commands it is just another forwarded argument, passed after the `--` separator. `bv` emits `Release` as an overridable default, so a forwarded `-c`/`-p:Configuration=` still wins (e.g. `bv build -- -c Debug` builds `Debug`). `bv release` keeps `-c`/`--configuration` as a parsed option, since it needs the value to locate build artifacts.
- `--main-branch` is now a global option: it is accepted at any position on the command line (before or after the subcommand name), appears in the `GLOBAL OPTIONS` section of help, and is honored by the commands that talk to Git.
- **BREAKING CHANGE**: The `-c`/`--configuration` option is no longer parsed by `bv restore`/`build`/`test`/`pack`; for those commands it is just another forwarded argument, passed after the `--` separator. `bv` emits the configured default build configuration (`dotnet.configuration` in `buildvana.json`, or `Release`) as an overridable default, so a forwarded `-c`/`-p:Configuration=` still wins (e.g. `bv build -- -c Debug` builds `Debug`). `bv release` keeps `-c`/`--configuration` as a parsed option, since it needs the value to locate build artifacts.
- **BREAKING CHANGE**: The `--main-branch` global option has been removed, along with `bv`'s main-branch discovery. Documentation generation on release is now gated by `release.generateDocsFrom` in `buildvana.json`: a list of regular expressions matched against the current short branch name (default `["^main$", "^master$"]`, reproducing the previous main/master discovery). The human-curated changelog permalink in generated release notes now points at the release branch itself rather than the discovered main branch.
- **BREAKING CHANGE**: The `--unstable-changelog` and `--require-changelog` options of `bv release` have been removed with no CLI replacement; changelog policy is repository-stable, not per-invocation. Configure it in `buildvana.json` instead: `release.changelogUpdates` (`none` | `stable` | `all`, default `stable`) selects which releases update the changelog, and `release.emptyChangelog` provides substitute text for an empty "Unreleased changes" section (when unset, an empty section fails the release, matching the previous `--require-changelog` default of `true`).
- `bv`'s build commands (`clean`, `restore`, `build`, `test`, `pack`) and `release` now observe cancellation. Pressing Ctrl-C (or a host cancelling the operation) stops the pipeline promptly: it stops launching further steps and terminates the running `dotnet` child process instead of waiting for it to finish, then `bv` exits with code 130. Partial build output may be left behind on cancellation; `bv clean` recovers.

### Bugs fixed in this release
Expand Down
5 changes: 5 additions & 0 deletions src/Buildvana.Core.Configuration/DotNetInvocationConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@

using System.Collections.Generic;
using System.ComponentModel;
using JetBrains.Annotations;

namespace Buildvana.Core.Configuration;

/// <summary>
/// Configures the extra arguments and environment variables for one kind of <c>dotnet</c> invocation.
/// </summary>
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public sealed record DotNetInvocationConfig
{
/// <summary>
Expand Down
29 changes: 14 additions & 15 deletions src/Buildvana.Tool/Build/BuildPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,9 @@ namespace Buildvana.Tool.Build;
/// </summary>
internal sealed class BuildPipeline
{
/// <summary>
/// The MSBuild configuration used when a caller does not specify one. It is emitted as an overridable
/// default, so a user-forwarded <c>-c</c>/<c>-p:Configuration=</c> wins.
/// </summary>
public const string DefaultConfiguration = "Release";

private readonly SolutionContext _solution;
private readonly DotNetService _dotnet;
private readonly DotNetSettings _dotnetSettings;
private readonly IReporter _reporter;
private readonly IReadOnlyList<string> _forwardedArgs;

Expand All @@ -39,15 +34,18 @@ internal sealed class BuildPipeline
public BuildPipeline(
SolutionContext solution,
DotNetService dotnet,
DotNetSettings dotnetSettings,
IReporter reporter,
CommandParameters parameters)
{
Guard.IsNotNull(solution);
Guard.IsNotNull(dotnet);
Guard.IsNotNull(dotnetSettings);
Guard.IsNotNull(reporter);
Guard.IsNotNull(parameters);
_solution = solution;
_dotnet = dotnet;
_dotnetSettings = dotnetSettings;
_reporter = reporter;
_forwardedArgs = parameters.Forwarded;
}
Expand All @@ -56,21 +54,21 @@ public BuildPipeline(
/// Runs the pipeline from <see cref="BuildStep.Clean"/> through <paramref name="last"/>, inclusive.
/// </summary>
/// <param name="last">The last step to run.</param>
/// <param name="configuration">The MSBuild configuration to build.</param>
/// <param name="configuration">The MSBuild configuration to build, or <see langword="null"/> to use the configured default (<see cref="DotNetSettings.Configuration"/>).</param>
/// <param name="cancellationToken">A token to observe while running the pipeline. When signalled, the pipeline stops launching further steps and the running <c>dotnet</c> child process is terminated.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
public Task RunThroughAsync(BuildStep last, string configuration = DefaultConfiguration, CancellationToken cancellationToken = default)
public Task RunThroughAsync(BuildStep last, string? configuration = null, CancellationToken cancellationToken = default)
=> RunRangeAsync(BuildStep.Clean, last, configuration, cancellationToken);

/// <summary>
/// Runs the pipeline from <paramref name="first"/> through <paramref name="last"/>, inclusive.
/// </summary>
/// <param name="first">The first step to run.</param>
/// <param name="last">The last step to run.</param>
/// <param name="configuration">The MSBuild configuration to build.</param>
/// <param name="configuration">The MSBuild configuration to build, or <see langword="null"/> to use the configured default (<see cref="DotNetSettings.Configuration"/>).</param>
/// <param name="cancellationToken">A token to observe while running the pipeline. When signalled, the pipeline stops launching further steps and the running <c>dotnet</c> child process is terminated.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
public async Task RunRangeAsync(BuildStep first, BuildStep last, string configuration = DefaultConfiguration, CancellationToken cancellationToken = default)
public async Task RunRangeAsync(BuildStep first, BuildStep last, string? configuration = null, CancellationToken cancellationToken = default)
{
Guard.IsLessThanOrEqualTo((int)first, (int)last, nameof(first));
for (var step = first; step <= last; step++)
Expand All @@ -84,19 +82,20 @@ public async Task RunRangeAsync(BuildStep first, BuildStep last, string configur
/// Runs a single pipeline step.
/// </summary>
/// <param name="step">The step to run.</param>
/// <param name="configuration">The MSBuild configuration to build (ignored by <see cref="BuildStep.Clean"/> and <see cref="BuildStep.Restore"/>).</param>
/// <param name="configuration">The MSBuild configuration to build (ignored by <see cref="BuildStep.Clean"/> and <see cref="BuildStep.Restore"/>), or <see langword="null"/> to use the configured default (<see cref="DotNetSettings.Configuration"/>).</param>
/// <param name="cancellationToken">A token to observe while running the step. When signalled, the running <c>dotnet</c> child process is terminated.</param>
/// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
public async Task RunAsync(BuildStep step, string configuration = DefaultConfiguration, CancellationToken cancellationToken = default)
public async Task RunAsync(BuildStep step, string? configuration = null, CancellationToken cancellationToken = default)
{
var resolvedConfiguration = configuration ?? _dotnetSettings.Configuration;
using var activity = _reporter.BeginActivity(step.ToString());
var task = step switch
{
BuildStep.Clean => CleanAsync(cancellationToken),
BuildStep.Restore => RestoreAsync(cancellationToken),
BuildStep.Build => BuildAsync(configuration, cancellationToken),
BuildStep.Test => TestAsync(configuration, cancellationToken),
BuildStep.Pack => PackAsync(configuration, cancellationToken),
BuildStep.Build => BuildAsync(resolvedConfiguration, cancellationToken),
BuildStep.Test => TestAsync(resolvedConfiguration, cancellationToken),
BuildStep.Pack => PackAsync(resolvedConfiguration, cancellationToken),
_ => ThrowHelper.ThrowArgumentOutOfRangeException<Task>(nameof(step), step, "Unknown build step."),
};
await task.ConfigureAwait(false);
Expand Down
3 changes: 1 addition & 2 deletions src/Buildvana.Tool/CommandLine/CliArgSplitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,12 @@ public static ParsedCommandLine Split(IReadOnlyList<string> args)

var reader = new CliOptionReader(working);
var verbosity = reader.ReadValue("--verbosity", "-v");
var mainBranch = reader.ReadValue("--main-branch");
var color = reader.ReadFlag("--color");
var noColor = reader.ReadFlag("--no-color");
var nologo = reader.ReadFlag("--nologo");
var version = reader.ReadFlag("--version");
var helpRequested = reader.ReadFlag("--help", "-h");
var globals = new GlobalSettings(verbosity, mainBranch, color, noColor, nologo, version);
var globals = new GlobalSettings(verbosity, color, noColor, nologo, version);

var (subcommand, positionals, optionTokens) = Classify(reader.Remaining);
return new ParsedCommandLine(globals, helpRequested, subcommand, positionals, optionTokens, forwarded);
Expand Down
9 changes: 6 additions & 3 deletions src/Buildvana.Tool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,16 @@ private static ServiceProvider BuildServiceProvider(
.AddSingleton(reporter)
.AddSingleton(globals)
.AddSingleton(new CommandParameters(parsed.OptionTokens, parsed.Forwarded))
.AddSingleton(static sp => ReleaseSettings.Parse(sp.GetRequiredService<CommandParameters>().Options))
.AddSingleton(static sp => ReleaseSettings.Parse(
sp.GetRequiredService<CommandParameters>().Options,
sp.GetRequiredService<BuildvanaConfig>(),
sp.GetRequiredService<DotNetSettings>()))
.AddSingleton<IHomeDirectoryProvider>(static _ => new DiscoveredHomeDirectoryProvider(Environment.CurrentDirectory))

// Lazy by design: this factory (and thus discovery, parsing, and validation) runs on first resolve.
// No Phase 1 command resolves BuildvanaConfig, so a malformed buildvana.json stays inert until a
// Phase 2 consumer reads it.
// A malformed buildvana.json stays inert until a consumer (e.g. DotNetSettings or ReleaseSettings) reads it.
.AddSingleton(static sp => BuildvanaConfigLoader.Load(sp.GetRequiredService<IHomeDirectoryProvider>().HomeDirectory))
.AddSingleton<DotNetSettings>()
.AddSingleton<IJsonHelper, JsonHelper>()
.AddSingleton<IProcessRunner, ProcessRunner>()
.AddSingleton<ISolutionContextFactory, HomeDirectorySolutionContextFactory>()
Expand Down
12 changes: 11 additions & 1 deletion src/Buildvana.Tool/Services/ChangelogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,136 +92,146 @@
/// Prepares the changelog for a new release by moving the contents of the "Unreleased changes" section
/// to a new section.
/// </summary>
public void PrepareForRelease()
/// <param name="emptyChangelogSubstitute">Text to use as the new section's body when the "Unreleased changes"
/// section has no content. When <see langword="null"/>, an empty section is moved verbatim (producing a
/// title-only section).</param>
public void PrepareForRelease(string? emptyChangelogSubstitute = null)
{
_reporter.Info("Updating changelog...");
var encoding = new UTF8Encoding(false, true);
var sb = new StringBuilder();
using (var reader = new StreamReader(FileName, encoding))
using (var writer = new StringWriter(sb, CultureInfo.InvariantCulture))
{
// Using a StringWriter instead of a StringBuilder allows for a custom line separator
// Under Windows, a StringBuilder would only use "\r\n" as a line separator, which would be wrong in this case
writer.NewLine = "\n";
var sectionHeadingRegex = GetSectionHeadingRegex();
var subSectionHeadingRegex = GetSubsectionHeadingRegex();
var subSections = new List<(string Header, List<string> Lines)> { (string.Empty, []) };
var subSectionIndex = 0;

const int readingFileHeader = 0;
const int readingUnreleasedChangesSection = 1;
const int readingRemainderOfFile = 2;
const int readingDone = 3;
var state = readingFileHeader;
while (state != readingDone)
{
var line = reader.ReadLine();
switch (state)
{
case readingFileHeader:
BuildFailedException.ThrowIfNot(line != null, $"{FileName} contains no sections.");

// Copy everything up to an including the first section heading (which we assume is "Unreleased changes")
writer.WriteLine(line);
if (sectionHeadingRegex.IsMatch(line))
{
state = readingUnreleasedChangesSection;
}

break;
case readingUnreleasedChangesSection:
if (line == null)
{
// The changelog only contains the "Unreleased changes" section;
// this happens when no release has been published yet
WriteNewSections(true);
state = readingDone;
break;
}

if (sectionHeadingRegex.IsMatch(line))
{
// Reached header of next section
WriteNewSections(false);
writer.WriteLine(line);
state = readingRemainderOfFile;
break;
}

if (subSectionHeadingRegex.IsMatch(line))
{
subSections.Add((line, []));
++subSectionIndex;
break;
}

subSections[subSectionIndex].Lines.Add(line);
break;
case readingRemainderOfFile:
if (line == null)
{
state = readingDone;
break;
}

writer.WriteLine(line);
break;
}
}

void WriteNewSections(bool atEndOfFile)
{
// Create empty subsections in new "Unreleased changes" section
foreach (var (header, _) in subSections.Skip(1))
{
writer.WriteLine(string.Empty);
writer.WriteLine(header);
}

// Write header of new release section
writer.WriteLine(string.Empty);
writer.WriteLine("## " + MakeSectionTitle());

var newSectionLines = CollectNewSectionLines();
var newSectionCount = newSectionLines.Count;
if (atEndOfFile)
{
// If there is no other section after the new release,
// we don't want extra blank lines at EOF
while (newSectionCount > 0 && string.IsNullOrEmpty(newSectionLines[newSectionCount - 1]))
{
--newSectionCount;
}
}

foreach (var newSectionLine in newSectionLines.Take(newSectionCount))
{
writer.WriteLine(newSectionLine);
}
}

List<string> CollectNewSectionLines()
{
var result = new List<string>(subSections[0].Lines);

// Copy only subsections that have actual content
foreach (var (header, lines) in subSections.Skip(1).Where(s => s.Lines.Any(l => !string.IsNullOrWhiteSpace(l))))
{
result.Add(header);
result.AddRange(lines);
}

// When the "Unreleased changes" section has no real content, substitute the configured text (if any).
if (emptyChangelogSubstitute is not null && result.All(string.IsNullOrWhiteSpace))
{
result.Clear();
result.AddRange(emptyChangelogSubstitute.ReplaceLineEndings("\n").Split('\n'));
}

return result;
}
}

File.WriteAllText(FileName, sb.ToString(), encoding);
}

/// <summary>
/// Updates the heading of the first section of the changelog after the "Unreleased changes" section
/// to reflect a change in the released version.
/// </summary>

Check notice on line 234 in src/Buildvana.Tool/Services/ChangelogService.cs

View check run for this annotation

codefactor.io / CodeFactor

src/Buildvana.Tool/Services/ChangelogService.cs#L98-L234

Complex Method
public void UpdateNewSectionTitle()
{
_reporter.Info("Updating changelog's new release section title...");
Expand Down
23 changes: 23 additions & 0 deletions src/Buildvana.Tool/Services/DotNetInvocationSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
// See the LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace Buildvana.Tool.Services;

/// <summary>
/// The resolved extra arguments and environment variables for one kind of <c>dotnet</c> invocation,
/// taken from a <see cref="Buildvana.Core.Configuration.DotNetInvocationConfig"/>.
/// </summary>
/// <param name="Args">Extra arguments appended to the invocation.</param>
/// <param name="Env">Environment variables applied on top of the inherited environment, keyed by variable name.</param>
internal sealed record DotNetInvocationSettings(
IReadOnlyList<string> Args,
IReadOnlyDictionary<string, string?> Env)
{
/// <summary>
/// Gets an empty set of invocation settings (no extra arguments, no environment variables).
/// </summary>
public static DotNetInvocationSettings Empty { get; } = new([], ReadOnlyDictionary<string, string?>.Empty);
}
Loading