diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b68d55..7792930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,31 @@ 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 [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: `--versionSpecChange` → `--bump`, `--checkPublicApiFiles` → `--check-public-api`, `--updateChangelogOnPrerelease` → `--unstable-changelog`, `--ensureChangelogNotEmpty` → `--require-changelog`, `--updateSelfReferences` → `--dogfood`. The matching default-source environment variables follow the same renaming. +- **BREAKING CHANGE**: Five `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. + Secrets and endpoints (`GITHUB_TOKEN`, `PRIVATE_NUGET_SOURCE`/`KEY`, `PRERELEASE_NUGET_SOURCE`/`KEY`, `RELEASE_NUGET_SOURCE`/`KEY`) are unaffected. +- **BREAKING CHANGE**: `bv`'s `--verbosity` setting now accepts the same values as the .NET CLI: + - `quiet` (or `q`) + - `minimal` (or `m`) + - `normal` (or `n`) + - `detailed` (or `d`) + - `diagnostic` (or `diag`) + + Cake verbosity values (e.g., `verbose`) are no longer accepted. ### Bugs fixed in this release diff --git a/src/Buildvana.Tool/Cli/BaseSettings-Apply.cs b/src/Buildvana.Tool/Cli/BaseSettings-Apply.cs new file mode 100644 index 0000000..c237261 --- /dev/null +++ b/src/Buildvana.Tool/Cli/BaseSettings-Apply.cs @@ -0,0 +1,55 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using Buildvana.Core; +using Buildvana.Tool.Infrastructure.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Spectre.Console; + +namespace Buildvana.Tool.Cli; + +partial class BaseSettings +{ + /// + /// Applies the global options carried by this settings object to the runtime services: + /// logger min level (from ) and console color profile (from /). + /// + /// The service provider. + public void Apply(IServiceProvider services) + { + ApplyVerbosity(services); + ApplyColor(services); + } + + private static LogLevel ParseVerbosity(string raw) => raw.ToUpperInvariant() switch + { + "QUIET" or "Q" => LogLevel.Error, + "MINIMAL" or "M" => LogLevel.Warning, + "NORMAL" or "N" => LogLevel.Information, + "DETAILED" or "D" => LogLevel.Debug, + "DIAGNOSTIC" or "DIAG" => LogLevel.Trace, + _ => throw new BuildFailedException($"Unknown verbosity level '{raw}'. Use one of: quiet, minimal, normal, detailed, diagnostic."), + }; + + private void ApplyVerbosity(IServiceProvider services) + { + if (string.IsNullOrEmpty(Verbosity)) + { + return; + } + + services.GetRequiredService().MinLevel = ParseVerbosity(Verbosity); + } + + private void ApplyColor(IServiceProvider services) + { + if (Color == NoColor) + { + return; + } + + services.GetRequiredService().Profile.Capabilities.Ansi = Color; + } +} diff --git a/src/Buildvana.Tool/Cli/BaseSettings.cs b/src/Buildvana.Tool/Cli/BaseSettings.cs index 3a0bc34..8791fa0 100644 --- a/src/Buildvana.Tool/Cli/BaseSettings.cs +++ b/src/Buildvana.Tool/Cli/BaseSettings.cs @@ -11,13 +11,13 @@ namespace Buildvana.Tool.Cli; /// Global options shared by every Spectre command. /// [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] -public class BaseSettings : CommandSettings +public partial class BaseSettings : CommandSettings { /// /// Gets the requested logging verbosity. /// [CommandOption("-v|--verbosity ")] - [Description("Logging verbosity. Accepts either Trace/Debug/Information/Warning/Error/Critical/None or Quiet/Minimal/Normal/Verbose/Diagnostic.")] + [Description("Logging verbosity. One of: quiet, minimal, normal, detailed, diagnostic. Defaults to normal.")] public string? Verbosity { get; init; } /// diff --git a/src/Buildvana.Tool/Cli/BuildCommand.cs b/src/Buildvana.Tool/Cli/BuildCommand.cs index 16f9d46..2b915d5 100644 --- a/src/Buildvana.Tool/Cli/BuildCommand.cs +++ b/src/Buildvana.Tool/Cli/BuildCommand.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; +using Microsoft.Extensions.DependencyInjection; using Spectre.Console.Cli; namespace Buildvana.Tool.Cli; @@ -16,7 +17,8 @@ internal sealed class BuildCommand(IServiceProvider services) : AsyncCommand ExecuteAsync(CommandContext context, BuildSettings settings, CancellationToken cancellationToken) { Guard.IsNotNull(settings); - SettingsApplier.Apply(settings, services); + settings.Apply(services); + services.GetRequiredService().Current = settings; await BuildSteps.CleanAsync(services).ConfigureAwait(false); await BuildSteps.RestoreAsync(services).ConfigureAwait(false); await BuildSteps.BuildAsync(services).ConfigureAwait(false); diff --git a/src/Buildvana.Tool/Cli/BuildSettings.cs b/src/Buildvana.Tool/Cli/BuildSettings.cs index c285cfb..0ceb919 100644 --- a/src/Buildvana.Tool/Cli/BuildSettings.cs +++ b/src/Buildvana.Tool/Cli/BuildSettings.cs @@ -17,13 +17,24 @@ public class BuildSettings : BaseSettings /// Gets the MSBuild configuration to build. /// [CommandOption("-c|--configuration ")] - [Description("MSBuild configuration to build. Defaults to 'Release' (or the CONFIGURATION environment variable, if set).")] + [Description("MSBuild configuration to build. Defaults to 'Release'.")] public string? Configuration { get; init; } /// /// Gets the name of the repository's main branch. /// [CommandOption("--main-branch ")] - [Description("Name of the repository's main branch. Defaults to 'main' (or the MAIN_BRANCH environment variable, if set).")] + [Description("Name of the repository's main branch. Defaults to 'main'.")] public string? MainBranch { get; init; } + + /// + /// Gets the resolved MSBuild configuration: if set, otherwise "Release". + /// + public string ResolveConfiguration() => Configuration ?? "Release"; + + /// + /// Gets the configured main branch name, or the empty string if none was passed (in which case + /// GitService auto-detects from main/master). + /// + public string ResolveMainBranch() => MainBranch ?? string.Empty; } diff --git a/src/Buildvana.Tool/Cli/BuildSettingsHolder.cs b/src/Buildvana.Tool/Cli/BuildSettingsHolder.cs new file mode 100644 index 0000000..2bdbff8 --- /dev/null +++ b/src/Buildvana.Tool/Cli/BuildSettingsHolder.cs @@ -0,0 +1,22 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; + +namespace Buildvana.Tool.Cli; + +/// +/// Singleton holder for the current command's . +/// +/// +/// The active command's ExecuteAsync populates before any service that +/// reads build options is resolved. +/// +public sealed class BuildSettingsHolder +{ + public BuildSettings Current + { + get => field ?? throw new InvalidOperationException("BuildSettingsHolder.Current was read before it was set."); + set; + } +} diff --git a/src/Buildvana.Tool/Cli/CleanCommand.cs b/src/Buildvana.Tool/Cli/CleanCommand.cs index 28e40d1..c73248b 100644 --- a/src/Buildvana.Tool/Cli/CleanCommand.cs +++ b/src/Buildvana.Tool/Cli/CleanCommand.cs @@ -16,7 +16,7 @@ internal sealed class CleanCommand(IServiceProvider services) : AsyncCommand ExecuteAsync(CommandContext context, BaseSettings settings, CancellationToken cancellationToken) { Guard.IsNotNull(settings); - SettingsApplier.Apply(settings, services); + settings.Apply(services); await BuildSteps.CleanAsync(services).ConfigureAwait(false); return 0; } diff --git a/src/Buildvana.Tool/Cli/PackCommand.cs b/src/Buildvana.Tool/Cli/PackCommand.cs index 03604fe..5494cef 100644 --- a/src/Buildvana.Tool/Cli/PackCommand.cs +++ b/src/Buildvana.Tool/Cli/PackCommand.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; +using Microsoft.Extensions.DependencyInjection; using Spectre.Console.Cli; namespace Buildvana.Tool.Cli; @@ -16,7 +17,8 @@ internal sealed class PackCommand(IServiceProvider services) : AsyncCommand ExecuteAsync(CommandContext context, BuildSettings settings, CancellationToken cancellationToken) { Guard.IsNotNull(settings); - SettingsApplier.Apply(settings, services); + settings.Apply(services); + services.GetRequiredService().Current = settings; await BuildSteps.CleanAsync(services).ConfigureAwait(false); await BuildSteps.RestoreAsync(services).ConfigureAwait(false); await BuildSteps.BuildAsync(services).ConfigureAwait(false); diff --git a/src/Buildvana.Tool/Cli/ReleaseCommand.cs b/src/Buildvana.Tool/Cli/ReleaseCommand.cs index 4054869..a4aa7eb 100644 --- a/src/Buildvana.Tool/Cli/ReleaseCommand.cs +++ b/src/Buildvana.Tool/Cli/ReleaseCommand.cs @@ -30,7 +30,8 @@ internal sealed class ReleaseCommand(IServiceProvider services) : AsyncCommand ExecuteAsync(CommandContext context, ReleaseSettings settings, CancellationToken cancellationToken) { Guard.IsNotNull(settings); - SettingsApplier.Apply(settings, services); + settings.Apply(services); + services.GetRequiredService().Current = settings; // Pre-pipeline (mirrors today's [IsDependentOn(TestTask)] chain). await BuildSteps.CleanAsync(services).ConfigureAwait(false); @@ -41,7 +42,6 @@ protected override async Task ExecuteAsync(CommandContext context, ReleaseS var logger = services.GetRequiredService().CreateLogger("Release"); var home = services.GetRequiredService(); var jsonHelper = services.GetRequiredService(); - var options = services.GetRequiredService(); var server = services.GetRequiredService(); var version = services.GetRequiredService(); var dotnet = services.GetRequiredService(); @@ -82,8 +82,8 @@ protected override async Task ExecuteAsync(CommandContext context, ReleaseS // Compute the version spec change to apply, if any. // This implies more checks and possibly throws, so do it as early as possible. var versionSpecChange = version.ComputeVersionSpecChange( - requestedChange: options.GetOption("bump", VersionSpecChange.None), - checkPublicApiFiles: options.GetOption("checkPublicApi", true)); + requestedChange: settings.ResolveBump(), + checkPublicApiFiles: settings.ResolveCheckPublicApi()); var release = await server.CreateReleaseAsync().ConfigureAwait(false); await using (release.ConfigureAwait(false)) @@ -138,9 +138,9 @@ protected override async Task ExecuteAsync(CommandContext context, ReleaseS { logger.LogInformation("Changelog update skipped: {Path} not found.", ChangelogService.FileName); } - else if (!version.IsPrerelease || options.GetOption("unstableChangelog", false)) + else if (!version.IsPrerelease || settings.ResolveUnstableChangelog()) { - if (options.GetOption("requireChangelog", true)) + if (settings.ResolveRequireChangelog()) { BuildFailedException.ThrowIfNot( changelog.HasUnreleasedChanges(), @@ -196,7 +196,7 @@ protected override async Task ExecuteAsync(CommandContext context, ReleaseS // Goes into a separate commit so the tagged "Prepare release" commit reflects the actual built // state (which still references the previously-published versions); the dogfood commit is marked // [skip ci] because the new packages aren't in the feed yet at push time. - if (options.GetOption("dogfood", true)) + if (settings.ResolveDogfood()) { var selfReferenceUpdates = selfReferenceUpdater.UpdateReferences(); switch (selfReferenceUpdates.Count) diff --git a/src/Buildvana.Tool/Cli/ReleaseSettings.cs b/src/Buildvana.Tool/Cli/ReleaseSettings.cs index 72b79bb..b64267e 100644 --- a/src/Buildvana.Tool/Cli/ReleaseSettings.cs +++ b/src/Buildvana.Tool/Cli/ReleaseSettings.cs @@ -1,7 +1,10 @@ // Copyright (C) Tenacom and Contributors. Licensed under the MIT license. // See the LICENSE file in the project root for full license information. +using System; using System.ComponentModel; +using Buildvana.Core; +using Buildvana.Tool.Services.Versioning; using JetBrains.Annotations; using Spectre.Console.Cli; @@ -54,4 +57,41 @@ public class ReleaseSettings : BuildSettings [CommandOption("--dogfood ")] [Description("Update in-tree references to packages produced by this release. Defaults to true.")] public bool? Dogfood { get; init; } + + /// + /// Parses into a ; defaults to . + /// + /// The value of is not a recognized version-spec change. + public VersionSpecChange ResolveBump() + { + if (Bump is null) + { + return VersionSpecChange.None; + } + + var parsed = Enum.TryParse(Bump, ignoreCase: true, out var value) && Enum.IsDefined(value); + return parsed + ? value + : throw new BuildFailedException($"Invalid value '{Bump}' for --bump. Valid values: none, unstable, stable, minor, major."); + } + + /// + /// Returns if set, otherwise . + /// + public bool ResolveCheckPublicApi() => CheckPublicApi ?? true; + + /// + /// Returns if set, otherwise . + /// + public bool ResolveUnstableChangelog() => UnstableChangelog ?? false; + + /// + /// Returns if set, otherwise . + /// + public bool ResolveRequireChangelog() => RequireChangelog ?? true; + + /// + /// Returns if set, otherwise . + /// + public bool ResolveDogfood() => Dogfood ?? true; } diff --git a/src/Buildvana.Tool/Cli/RestoreCommand.cs b/src/Buildvana.Tool/Cli/RestoreCommand.cs index 0c15cee..a6073e4 100644 --- a/src/Buildvana.Tool/Cli/RestoreCommand.cs +++ b/src/Buildvana.Tool/Cli/RestoreCommand.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; +using Microsoft.Extensions.DependencyInjection; using Spectre.Console.Cli; namespace Buildvana.Tool.Cli; @@ -16,7 +17,8 @@ internal sealed class RestoreCommand(IServiceProvider services) : AsyncCommand ExecuteAsync(CommandContext context, BuildSettings settings, CancellationToken cancellationToken) { Guard.IsNotNull(settings); - SettingsApplier.Apply(settings, services); + settings.Apply(services); + services.GetRequiredService().Current = settings; await BuildSteps.CleanAsync(services).ConfigureAwait(false); await BuildSteps.RestoreAsync(services).ConfigureAwait(false); return 0; diff --git a/src/Buildvana.Tool/Cli/SettingsApplier.cs b/src/Buildvana.Tool/Cli/SettingsApplier.cs deleted file mode 100644 index 9e9e1ad..0000000 --- a/src/Buildvana.Tool/Cli/SettingsApplier.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. -// See the LICENSE file in the project root for full license information. - -using System; -using Buildvana.Core; -using Buildvana.Tool.Infrastructure.Logging; -using Buildvana.Tool.Services; -using CommunityToolkit.Diagnostics; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Spectre.Console; -using Spectre.Console.Cli; - -namespace Buildvana.Tool.Cli; - -/// -/// Applies parsed Spectre command settings to runtime services: logger min level, console color profile, -/// and named-option pushes into . -/// -internal static class SettingsApplier -{ - public static void Apply(CommandSettings settings, IServiceProvider services) - { - Guard.IsNotNull(settings); - Guard.IsNotNull(services); - - if (settings is BaseSettings @base) - { - ApplyVerbosity(@base.Verbosity, services); - ApplyColor(@base.Color, @base.NoColor, services); - } - - var options = services.GetRequiredService(); - if (settings is BuildSettings build) - { - SetIfPresent(options, "configuration", build.Configuration); - SetIfPresent(options, "mainBranch", build.MainBranch); - } - - if (settings is ReleaseSettings release) - { - SetIfPresent(options, "bump", release.Bump); - SetIfPresent(options, "checkPublicApi", release.CheckPublicApi); - SetIfPresent(options, "unstableChangelog", release.UnstableChangelog); - SetIfPresent(options, "requireChangelog", release.RequireChangelog); - SetIfPresent(options, "dogfood", release.Dogfood); - } - } - - private static void SetIfPresent(OptionsService options, string name, string? value) - { - if (!string.IsNullOrEmpty(value)) - { - options.SetOption(name, value); - } - } - - private static void SetIfPresent(OptionsService options, string name, bool? value) - { - if (value.HasValue) - { - options.SetOption(name, value.Value ? "true" : "false"); - } - } - - private static void ApplyVerbosity(string? raw, IServiceProvider services) - { - if (string.IsNullOrEmpty(raw)) - { - return; - } - - var level = ParseVerbosity(raw); - services.GetRequiredService().MinLevel = level; - } - - private static LogLevel ParseVerbosity(string raw) => raw.ToUpperInvariant() switch - { - "QUIET" => LogLevel.Critical, - "MINIMAL" => LogLevel.Warning, - "NORMAL" => LogLevel.Information, - "VERBOSE" or "DEBUG" => LogLevel.Debug, - "DIAGNOSTIC" or "TRACE" => LogLevel.Trace, - "INFO" or "INFORMATION" => LogLevel.Information, - "WARN" or "WARNING" => LogLevel.Warning, - "ERROR" => LogLevel.Error, - "CRITICAL" => LogLevel.Critical, - "NONE" => LogLevel.None, - _ => throw new BuildFailedException($"Unknown verbosity level '{raw}'. Use one of: Quiet, Minimal, Normal, Verbose, Diagnostic, Trace, Debug, Information, Warning, Error, Critical, None."), - }; - - private static void ApplyColor(bool color, bool noColor, IServiceProvider services) - { - if (color == noColor) - { - return; - } - - var console = services.GetRequiredService(); - console.Profile.Capabilities.Ansi = color; - } -} diff --git a/src/Buildvana.Tool/Cli/TestCommand.cs b/src/Buildvana.Tool/Cli/TestCommand.cs index c0a2148..a45e772 100644 --- a/src/Buildvana.Tool/Cli/TestCommand.cs +++ b/src/Buildvana.Tool/Cli/TestCommand.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; +using Microsoft.Extensions.DependencyInjection; using Spectre.Console.Cli; namespace Buildvana.Tool.Cli; @@ -16,7 +17,8 @@ internal sealed class TestCommand(IServiceProvider services) : AsyncCommand ExecuteAsync(CommandContext context, BuildSettings settings, CancellationToken cancellationToken) { Guard.IsNotNull(settings); - SettingsApplier.Apply(settings, services); + settings.Apply(services); + services.GetRequiredService().Current = settings; await BuildSteps.CleanAsync(services).ConfigureAwait(false); await BuildSteps.RestoreAsync(services).ConfigureAwait(false); await BuildSteps.BuildAsync(services).ConfigureAwait(false); diff --git a/src/Buildvana.Tool/Configuration/NuGetPushConfiguration.cs b/src/Buildvana.Tool/Configuration/NuGetPushConfiguration.cs new file mode 100644 index 0000000..bd08c35 --- /dev/null +++ b/src/Buildvana.Tool/Configuration/NuGetPushConfiguration.cs @@ -0,0 +1,29 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Buildvana.Core; + +namespace Buildvana.Tool.Configuration; + +/// +/// Typed configuration for NuGet package pushes: one push target per channel (private, prerelease, release). +/// +/// +/// This is the seedling of a future file-backed configuration layer. For now, values are read from +/// environment variables; when the file layer arrives, only changes. +/// +internal sealed record NuGetPushConfiguration( + NuGetPushTarget Private, + NuGetPushTarget Prerelease, + NuGetPushTarget Release) +{ + /// + /// Reads all push targets from environment variables. + /// + /// A populated . + /// A required environment variable is not set or empty. + public static NuGetPushConfiguration FromEnvironment() => new( + Private: NuGetPushTarget.FromEnvironment("PRIVATE"), + Prerelease: NuGetPushTarget.FromEnvironment("PRERELEASE"), + Release: NuGetPushTarget.FromEnvironment("RELEASE")); +} diff --git a/src/Buildvana.Tool/Configuration/NuGetPushTarget.cs b/src/Buildvana.Tool/Configuration/NuGetPushTarget.cs new file mode 100644 index 0000000..13b3feb --- /dev/null +++ b/src/Buildvana.Tool/Configuration/NuGetPushTarget.cs @@ -0,0 +1,21 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Buildvana.Tool.Utilities; + +namespace Buildvana.Tool.Configuration; + +/// +/// A NuGet push target: source URL and API key. +/// +internal sealed record NuGetPushTarget(string Source, string ApiKey) +{ + /// + /// Reads the source and API key from environment variables named {prefix}_NUGET_SOURCE and {prefix}_NUGET_KEY. + /// + /// The environment-variable name prefix (e.g., PRIVATE, PRERELEASE, RELEASE). + /// A populated . + public static NuGetPushTarget FromEnvironment(string prefix) => new( + Source: EnvVarHelper.Require($"{prefix}_NUGET_SOURCE"), + ApiKey: EnvVarHelper.Require($"{prefix}_NUGET_KEY")); +} diff --git a/src/Buildvana.Tool/Configuration/ToolConfiguration.cs b/src/Buildvana.Tool/Configuration/ToolConfiguration.cs new file mode 100644 index 0000000..4972e43 --- /dev/null +++ b/src/Buildvana.Tool/Configuration/ToolConfiguration.cs @@ -0,0 +1,25 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Buildvana.Core; +using Buildvana.Tool.Utilities; + +namespace Buildvana.Tool.Configuration; + +/// +/// Typed configuration for bv: secrets and endpoints with no CLI-flag counterpart. +/// +/// +/// This is the seedling of a future file-backed configuration layer. For now, values are read from +/// environment variables; when the file layer arrives, only changes. +/// +internal sealed record ToolConfiguration(string GitHubToken) +{ + /// + /// Reads all configuration values from environment variables. + /// + /// A populated . + /// A required environment variable is not set or empty. + public static ToolConfiguration FromEnvironment() => new( + GitHubToken: EnvVarHelper.Require("GITHUB_TOKEN")); +} diff --git a/src/Buildvana.Tool/Program.cs b/src/Buildvana.Tool/Program.cs index dbf94db..3387bc5 100644 --- a/src/Buildvana.Tool/Program.cs +++ b/src/Buildvana.Tool/Program.cs @@ -10,6 +10,7 @@ using Buildvana.Core.Json; using Buildvana.Core.Process; using Buildvana.Tool.Cli; +using Buildvana.Tool.Configuration; using Buildvana.Tool.Infrastructure.Logging; using Buildvana.Tool.Services; using Buildvana.Tool.Services.Git; @@ -53,7 +54,9 @@ public static async Task Main(string[] args) .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() + .AddSingleton(static _ => ToolConfiguration.FromEnvironment()) + .AddSingleton(static _ => NuGetPushConfiguration.FromEnvironment()) .AddSingleton(); var registrar = new TypeRegistrar(services); diff --git a/src/Buildvana.Tool/Services/DotNetService.cs b/src/Buildvana.Tool/Services/DotNetService.cs index 7327584..88e0b44 100644 --- a/src/Buildvana.Tool/Services/DotNetService.cs +++ b/src/Buildvana.Tool/Services/DotNetService.cs @@ -6,12 +6,15 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using Buildvana.Tool.Cli; +using Buildvana.Tool.Configuration; using Buildvana.Tool.Infrastructure; using Buildvana.Tool.Services.ServerAdapters; using Buildvana.Tool.Services.Solution; using Buildvana.Tool.Services.Versioning; using Buildvana.Tool.Utilities; using CommunityToolkit.Diagnostics; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using IProcessRunner = Buildvana.Core.Process.IProcessRunner; @@ -33,7 +36,7 @@ private static readonly string DotNetMuxer private readonly ILogger _logger; private readonly IProcessRunner _processRunner; - private readonly OptionsService _options; + private readonly IServiceProvider _services; private readonly ServerAdapter _server; private readonly VersionService _version; private readonly MSBuildProperties _msbuildProperties; @@ -44,24 +47,26 @@ private static readonly string DotNetMuxer public DotNetService( ILogger logger, IProcessRunner processRunner, - OptionsService options, + BuildSettingsHolder buildSettings, + IServiceProvider services, ServerAdapter server, VersionService version, MSBuildProperties msbuildProperties) { Guard.IsNotNull(logger); Guard.IsNotNull(processRunner); - Guard.IsNotNull(options); + Guard.IsNotNull(buildSettings); + Guard.IsNotNull(services); Guard.IsNotNull(server); Guard.IsNotNull(version); Guard.IsNotNull(msbuildProperties); _logger = logger; _processRunner = processRunner; - _options = options; + _services = services; _server = server; _version = version; _msbuildProperties = msbuildProperties; - Configuration = options.GetOption("configuration", "Release"); + Configuration = buildSettings.Current.ResolveConfiguration(); ArtifactsPath = Path.Combine(CommonPaths.AllArtifacts, Configuration); } @@ -207,15 +212,17 @@ public async Task NuGetPushAllAsync() } var isPrivate = await _server.IsPrivateRepositoryAsync().ConfigureAwait(false); - var nugetSource = _options.GetOptionOrFail(isPrivate ? "privateNugetSource" : _version.IsPrerelease ? "prereleaseNugetSource" : "releaseNugetSource"); - var nugetApiKey = _options.GetOptionOrFail(isPrivate ? "privateNugetKey" : _version.IsPrerelease ? "prereleaseNugetKey" : "releaseNugetKey"); + var nugetConfig = _services.GetRequiredService(); + var target = isPrivate ? nugetConfig.Private + : _version.IsPrerelease ? nugetConfig.Prerelease + : nugetConfig.Release; foreach (var path in packages) { - _logger.LogInformation("Pushing {Path} to {Source}...", path, nugetSource); + _logger.LogInformation("Pushing {Path} to {Source}...", path, target.Source); await _processRunner .RunAsync( DotNetMuxer, - ["nuget", "push", path, "--source", nugetSource, "--api-key", nugetApiKey, "--skip-duplicate", "--force-english-output"]) + ["nuget", "push", path, "--source", target.Source, "--api-key", target.ApiKey, "--skip-duplicate", "--force-english-output"]) .ConfigureAwait(false); } } diff --git a/src/Buildvana.Tool/Services/Git/GitService.cs b/src/Buildvana.Tool/Services/Git/GitService.cs index a98bc31..3de2c9f 100644 --- a/src/Buildvana.Tool/Services/Git/GitService.cs +++ b/src/Buildvana.Tool/Services/Git/GitService.cs @@ -8,6 +8,7 @@ using System.Linq; using Buildvana.Core; using Buildvana.Core.HomeDirectory; +using Buildvana.Tool.Cli; using CommunityToolkit.Diagnostics; using JetBrains.Annotations; using LibGit2Sharp; @@ -26,11 +27,11 @@ public sealed class GitService : IDisposable private readonly IHomeDirectoryProvider _home; private readonly Repository _repository; - public GitService(ILogger logger, IHomeDirectoryProvider home, OptionsService options) + public GitService(ILogger logger, IHomeDirectoryProvider home, BuildSettingsHolder buildSettings) { Guard.IsNotNull(logger); Guard.IsNotNull(home); - Guard.IsNotNull(options); + Guard.IsNotNull(buildSettings); _logger = logger; _home = home; var homeDirectory = home.HomeDirectory; @@ -41,8 +42,7 @@ public GitService(ILogger logger, IHomeDirectoryProvider home, Optio OriginUrl = new(originUrl); var headName = _repository.Head.CanonicalName; CurrentBranch = headName.StartsWith("refs/heads/", StringComparison.Ordinal) ? _repository.Head.FriendlyName : string.Empty; - var configuredMainBranch = options.GetOption("mainBranch", string.Empty); - MainBranch = FindMainBranch(origin, configuredMainBranch); + MainBranch = FindMainBranch(origin, buildSettings.Current.ResolveMainBranch()); } /// diff --git a/src/Buildvana.Tool/Services/OptionsService.cs b/src/Buildvana.Tool/Services/OptionsService.cs deleted file mode 100644 index 28dd52e..0000000 --- a/src/Buildvana.Tool/Services/OptionsService.cs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. -// See the LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; -using Buildvana.Core; -using CommunityToolkit.Diagnostics; - -namespace Buildvana.Tool.Services; - -public sealed partial class OptionsService -{ - // Regular expressions used by OptionNameToEnvironmentVariableName - private static readonly Regex UnderscoreCasingRegex1 = GetUnderscoreCasingRegex1(); - private static readonly Regex UnderscoreCasingRegex2 = GetUnderscoreCasingRegex2(); - private static readonly Regex UnderscoreCasingRegex3 = GetUnderscoreCasingRegex3(); - - private readonly Dictionary _options = []; - - /// - /// Sets an option. The value set by this method will take precedence over arguments and environment variables. - /// - /// The option name. - /// The option value. - public void SetOption(string name, string value) - { - Guard.IsNotNullOrEmpty(name); - Guard.IsNotNullOrEmpty(value); - _options[name] = value; - } - - /// - /// Tells whether the specified option is present, either as an explicitly set option, as an argument, or as an environment variable. - /// - /// The option name. - /// - /// If an option with the specified name has been explicitly set, . - /// If an argument with the specified name is present, . - /// If an environment variable with the specified name (converted to UNDERSCORE_UPPER_CASE) is present, . - /// Otherwise, . - /// - public bool HasOption(string name) => TryGetOptionString(name, out _); - - /// - /// Gets the value of an option from, in this order: - /// - /// options set via , or - /// a command line argument with the specified name, or - /// an environment variable with the specified name converted to UNDERSCORE_UPPER_CASE, or - /// the provided default value. - /// - /// - /// The type of the option value. - /// The option name. - /// The value returned if the option was was found. - /// The value of the option, converted to . - public T GetOption(string name, T defaultValue) - where T : notnull - => TryGetOptionString(name, out var stringValue) - ? ConvertOptionValue(stringValue) - : defaultValue; - - /// - /// Gets the value of an option from, in this order: - /// - /// options set via , or - /// a command line argument with the specified name, or - /// an environment variable with the specified name converted to UNDERSCORE_UPPER_CASE. - /// - /// If the option is not found, this method fails the build. - /// - /// The type of the option value. - /// The option name. - /// The value of the option, converted to . - /// The specified option was not found. - public T GetOptionOrFail(string name) - where T : notnull - => TryGetOptionString(name, out var stringValue) - ? ConvertOptionValue(stringValue) - : throw new BuildFailedException($"Option {name} / environment variable {OptionNameToEnvironmentVariableName(name)} not found or empty."); - - [GeneratedRegex("([A-Z]+)([A-Z][a-z])", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant)] - private static partial Regex GetUnderscoreCasingRegex1(); - - [GeneratedRegex("([a-z0-9])([A-Z])", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant)] - private static partial Regex GetUnderscoreCasingRegex2(); - - [GeneratedRegex(@"[-\s]", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.CultureInvariant)] - private static partial Regex GetUnderscoreCasingRegex3(); - - private static string OptionNameToEnvironmentVariableName(string name) - { - // minorVersionOfSQLServer2012SomeMoreWords-more stuff - name = UnderscoreCasingRegex1.Replace(name, "$1_$2"); // -> minorVersionOfSQL_Server2012SomeMoreWords-more stuff - name = UnderscoreCasingRegex2.Replace(name, "$1_$2"); // -> minor_Version_Of_SQL_Server2012_Some_More_Words-more_stuff - name = UnderscoreCasingRegex3.Replace(name, "_"); // -> minor_Version_Of_SQL_Server2012_Some_More_Words_more_stuff - return name.ToUpperInvariant(); // -> MINOR_VERSION_OF_SQL_SERVER2012_SOME_MORE_WORDS_MORE_STUFF - } - - private static T ConvertOptionValue(string value) - where T : notnull - { - var converter = TypeDescriptor.GetConverter(typeof(T)); - return (T)converter.ConvertFromInvariantString(value)!; - } - - private bool TryGetOptionString(string name, [MaybeNullWhen(false)] out string value) - { - Guard.IsNotNullOrEmpty(name); - if (_options.TryGetValue(name, out value)) - { - return true; - } - - value = Environment.GetEnvironmentVariable(OptionNameToEnvironmentVariableName(name)); - if (string.IsNullOrEmpty(value)) - { - return false; - } - - _options[name] = value; - return true; - } -} diff --git a/src/Buildvana.Tool/Services/ServerAdapters/Internal/GitHub/GitHubServerAdapter.cs b/src/Buildvana.Tool/Services/ServerAdapters/Internal/GitHub/GitHubServerAdapter.cs index 8cda5a2..9ccab8c 100644 --- a/src/Buildvana.Tool/Services/ServerAdapters/Internal/GitHub/GitHubServerAdapter.cs +++ b/src/Buildvana.Tool/Services/ServerAdapters/Internal/GitHub/GitHubServerAdapter.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using Buildvana.Core; +using Buildvana.Tool.Configuration; using Buildvana.Tool.Services.Git; using Buildvana.Tool.Services.Versioning; using CommunityToolkit.Diagnostics; @@ -39,7 +40,7 @@ private GitHubServerAdapter(IServiceProvider services) RepositoryOwner = originInfo.PathSegments[0]; RepositoryName = originInfo.PathSegments[1]; RepositoryUrl = new Uri($"https://{HostName}/{RepositoryOwner}/{RepositoryName}"); - _token = services.GetRequiredService().GetOptionOrFail("githubToken"); + _token = services.GetRequiredService().GitHubToken; } /// diff --git a/src/Buildvana.Tool/Utilities/EnvVarHelper.cs b/src/Buildvana.Tool/Utilities/EnvVarHelper.cs new file mode 100644 index 0000000..74503f9 --- /dev/null +++ b/src/Buildvana.Tool/Utilities/EnvVarHelper.cs @@ -0,0 +1,24 @@ +// Copyright (C) Tenacom and Contributors. Licensed under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using Buildvana.Core; + +namespace Buildvana.Tool.Utilities; + +/// +/// Provides helper methods for accessing environment variables. +/// +public static class EnvVarHelper +{ + /// + /// Returns the value of the named environment variable, or fails the build if it is not set or empty. + /// + /// The environment variable name. + /// The non-empty value of the environment variable. + /// The environment variable is not set or empty. + public static string Require(string name) + => Environment.GetEnvironmentVariable(name) is { Length: > 0 } v + ? v + : throw new BuildFailedException($"Required environment variable {name} is not set or empty."); +}