diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f87933f..fad4b29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,5 +31,8 @@ jobs: - name: Run check-deps in Demo Application run: dotnet run --project Neolution.DotNet.Console.Demo --no-build --configuration '${{ env.BUILD_CONFIGURATION }}' -- check-deps - - name: Test - run: dotnet test --no-build --verbosity normal --configuration '${{ env.BUILD_CONFIGURATION }}' + - name: Unit Tests + run: dotnet test --no-build --verbosity normal --configuration '${{ env.BUILD_CONFIGURATION }}' --filter FullyQualifiedName~UnitTests + + - name: Integration Tests + run: dotnet test --no-build --verbosity normal --configuration '${{ env.BUILD_CONFIGURATION }}' --filter FullyQualifiedName~IntegrationTests diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 3a283d7..c8b6d95 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -50,10 +50,39 @@ jobs: git config user.email release-bot@neolution.ch - name: install release-it with plugins - run: npm install -g release-it @release-it/keep-a-changelog + run: npm install -g release-it@16.3.0 @release-it/keep-a-changelog@2.1.0 + + - name: Check Node.js version + run: | + node --version + npm --version + echo "=== PACKAGE.JSON DEBUG ===" + if [ -f package.json ]; then + echo "package.json exists:" + cat package.json + else + echo "No package.json found" + fi + echo "=== RELEASE-IT CONFIG DEBUG ===" + if [ -f .release-it.json ]; then + echo ".release-it.json contents:" + cat .release-it.json + else + echo "No .release-it.json found" + fi + echo "================================" - name: run release-it run: | + echo "=== WORKFLOW INPUTS DEBUG ===" + echo "bump_version_number: '${{ github.event.inputs.bump_version_number }}'" + echo "versioning_phase: '${{ github.event.inputs.versioning_phase }}'" + echo "is_dry_run: '${{ github.event.inputs.is_dry_run }}'" + echo "GITHUB_TOKEN (first 10 chars): ${GITHUB_TOKEN:0:10}..." + echo "Node.js version: $(node --version)" + echo "release-it version: $(release-it --version)" + echo "================================" + params=() if [[ ${{ github.event.inputs.bump_version_number }} != "consecutive" ]]; then @@ -62,8 +91,6 @@ jobs: if [[ ${{ github.event.inputs.versioning_phase }} != "stable" ]]; then params+=(--preRelease=${{ github.event.inputs.versioning_phase }}) - params+=(--plugins.@release-it/keep-a-changelog.keepUnreleased) - params+=(--no-plugins.@release-it/keep-a-changelog.strictLatest) fi if [[ ${{ github.event.inputs.is_dry_run }} == "true" ]]; then @@ -71,8 +98,17 @@ jobs: fi params+=(--ci) + params+=(--verbose) + echo "=== RELEASE-IT EXECUTION ===" echo "command: release-it ${params[@]}" + echo "Working directory: $(pwd)" + echo "Git status:" + git status --porcelain + echo "Git tags (last 5):" + git tag --sort=-version:refname | head -5 + echo "================================" + release-it "${params[@]}" env: GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} diff --git a/.release-it.json b/.release-it.json index 3f22ba4..c2530de 100644 --- a/.release-it.json +++ b/.release-it.json @@ -7,14 +7,16 @@ "skipChecks": true }, "github": { - "release": true + "release": true, + "releaseName": "Release ${version}" }, "plugins": { "@release-it/keep-a-changelog": { "filename": "CHANGELOG.md", - "addVersionUrl": true, + "strictLatest": false, "addUnreleased": true, - "strictLatest": false + "head": "Unreleased", + "keepUnreleased": "${preRelease}" } }, "hooks": { diff --git a/CHANGELOG.md b/CHANGELOG.md index 96c89fc..f1f6272 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Fixed loading of `appsettings.Development.json` during `check-deps` runs by forcing the Development environment for dependency validation. - Updated Scrutor to v6.1.0. - Updated Microsoft.Extensions packages to latest patch versions. diff --git a/Neolution.DotNet.Console.Demo/Program.cs b/Neolution.DotNet.Console.Demo/Program.cs index 305c94d..eaeb986 100644 --- a/Neolution.DotNet.Console.Demo/Program.cs +++ b/Neolution.DotNet.Console.Demo/Program.cs @@ -15,8 +15,6 @@ public static async Task Main(string[] args) try { var builder = DotNetConsole.CreateDefaultBuilder(args); - DotNetConsoleLogger.Initialize(builder.Configuration); - var startup = new Startup(builder.Environment, builder.Configuration); startup.ConfigureServices(builder.Services); var console = builder.Build(); diff --git a/Neolution.DotNet.Console.IntegrationTests/CheckDepsConsoleTests.cs b/Neolution.DotNet.Console.IntegrationTests/CheckDepsConsoleTests.cs new file mode 100644 index 0000000..2c87f27 --- /dev/null +++ b/Neolution.DotNet.Console.IntegrationTests/CheckDepsConsoleTests.cs @@ -0,0 +1,84 @@ +namespace Neolution.DotNet.Console.IntegrationTests +{ + using System.Diagnostics; + using System.Threading.Tasks; + using Shouldly; + using Xunit; + + /// + /// Integration tests for the CheckDepsConsole scenario. + /// + public class CheckDepsConsoleTests : IClassFixture + { + /// + /// The fixture that provides solution and project paths. + /// + private readonly SolutionDirectoryFixture fixture; + + /// + /// Initializes a new instance of the class. + /// + /// The solution directory fixture. + public CheckDepsConsoleTests(SolutionDirectoryFixture fixture) + { + this.fixture = fixture; + } + + /// + /// Given the Demo app, when run with 'check-deps', then it prints the expected DI validation message. + /// + /// A task representing the asynchronous operation. + [Fact] + public async Task GivenDemoApp_WhenRunWithCheckDeps_ThenPrintsDependencyInjectionValidationSucceeded() + { + // Arrange + var restorePsi = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"restore \"{this.fixture.DemoProjectPath}\"", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using (var restoreProcess = Process.Start(restorePsi)) + { + if (restoreProcess is null) + { + throw new System.InvalidOperationException("Failed to start dotnet restore for Demo app."); + } + + await restoreProcess.StandardOutput.ReadToEndAsync(); + await restoreProcess.StandardError.ReadToEndAsync(); + await restoreProcess.WaitForExitAsync(); + restoreProcess.ExitCode.ShouldBe(0, "dotnet restore failed for Demo app"); + } + + var psi = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"run --project \"{this.fixture.DemoProjectPath}\" check-deps", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + // Act + using var process = Process.Start(psi); + if (process is null) + { + throw new System.InvalidOperationException("Failed to start process for Demo app."); + } + + var output = await process.StandardOutput.ReadToEndAsync(); + var error = await process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); + + // Assert + output.ShouldContain("Dependency injection validation succeeded. All registered services can be constructed and no DI issues were found."); + process.ExitCode.ShouldBe(0, $"Process exited with code {process.ExitCode}. Error: {error}"); + } + } +} diff --git a/Neolution.DotNet.Console.IntegrationTests/Neolution.DotNet.Console.IntegrationTests.csproj b/Neolution.DotNet.Console.IntegrationTests/Neolution.DotNet.Console.IntegrationTests.csproj new file mode 100644 index 0000000..2d8635c --- /dev/null +++ b/Neolution.DotNet.Console.IntegrationTests/Neolution.DotNet.Console.IntegrationTests.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + false + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Neolution.DotNet.Console.IntegrationTests/SolutionDirectoryFixture.cs b/Neolution.DotNet.Console.IntegrationTests/SolutionDirectoryFixture.cs new file mode 100644 index 0000000..f73d8dd --- /dev/null +++ b/Neolution.DotNet.Console.IntegrationTests/SolutionDirectoryFixture.cs @@ -0,0 +1,42 @@ +namespace Neolution.DotNet.Console.IntegrationTests +{ + using System.IO; + + /// + /// Provides the solution directory and Demo project path for integration tests. + /// + public class SolutionDirectoryFixture + { + /// + /// Initializes a new instance of the class. + /// + public SolutionDirectoryFixture() + { + var dir = Directory.GetCurrentDirectory(); + while (dir != null && !File.Exists(Path.Combine(dir, "Neolution.DotNet.Console.sln"))) + { + dir = Path.GetDirectoryName(dir); + } + + // Set the solution directory to the one containing the solution file + this.SolutionDirectory = dir ?? throw new DirectoryNotFoundException("Could not find solution directory."); + + // Set the Demo project path relative to the solution directory + this.DemoProjectPath = Path.Combine(this.SolutionDirectory, "Neolution.DotNet.Console.Demo", "Neolution.DotNet.Console.Demo.csproj"); + if (!File.Exists(this.DemoProjectPath)) + { + throw new FileNotFoundException($"Demo project file not found: {this.DemoProjectPath}"); + } + } + + /// + /// Gets the solution directory path. + /// + public string SolutionDirectory { get; } + + /// + /// Gets the Demo project file path. + /// + public string DemoProjectPath { get; } + } +} diff --git a/Neolution.DotNet.Console.UnitTests/DotNetConsoleBuilderTests.cs b/Neolution.DotNet.Console.UnitTests/DotNetConsoleBuilderTests.cs index e3544f0..cd914b9 100644 --- a/Neolution.DotNet.Console.UnitTests/DotNetConsoleBuilderTests.cs +++ b/Neolution.DotNet.Console.UnitTests/DotNetConsoleBuilderTests.cs @@ -17,7 +17,7 @@ public class DotNetConsoleBuilderTests /// /// The argument string for the internal check-deps command /// - private const string CheckDependenciesArgumentString = "check-deps"; + private const string CheckDependenciesArgumentString = DotNetConsoleDefaults.CheckDependenciesCommand; /// /// Given a mistyped verb, when a default verb is defined, then should throw on console building. diff --git a/Neolution.DotNet.Console.sln b/Neolution.DotNet.Console.sln index 67bf04b..5e64a93 100644 --- a/Neolution.DotNet.Console.sln +++ b/Neolution.DotNet.Console.sln @@ -22,6 +22,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neolution.DotNet.Console.Un EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neolution.DotNet.Console.Demo", "Neolution.DotNet.Console.Demo\Neolution.DotNet.Console.Demo.csproj", "{3F17699A-2864-0EEC-AC50-93648D6E5BDE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neolution.DotNet.Console.IntegrationTests", "Neolution.DotNet.Console.IntegrationTests\Neolution.DotNet.Console.IntegrationTests.csproj", "{8C6D9105-CEF3-66FD-11A3-73BFF67273F6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -40,6 +42,10 @@ Global {3F17699A-2864-0EEC-AC50-93648D6E5BDE}.Debug|Any CPU.Build.0 = Debug|Any CPU {3F17699A-2864-0EEC-AC50-93648D6E5BDE}.Release|Any CPU.ActiveCfg = Release|Any CPU {3F17699A-2864-0EEC-AC50-93648D6E5BDE}.Release|Any CPU.Build.0 = Release|Any CPU + {8C6D9105-CEF3-66FD-11A3-73BFF67273F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C6D9105-CEF3-66FD-11A3-73BFF67273F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C6D9105-CEF3-66FD-11A3-73BFF67273F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C6D9105-CEF3-66FD-11A3-73BFF67273F6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Neolution.DotNet.Console/DotNetConsoleBuilder.cs b/Neolution.DotNet.Console/DotNetConsoleBuilder.cs index 442f4d0..fbe4316 100644 --- a/Neolution.DotNet.Console/DotNetConsoleBuilder.cs +++ b/Neolution.DotNet.Console/DotNetConsoleBuilder.cs @@ -85,12 +85,14 @@ public IDotNetConsole Build() if (this.checkDependencies) { - // Use development environment before building because that's where ValidateScopes and ValidateOnBuild are enabled. + // Ensure development environment is used for dependency checking + // Note: The environment should already be set to Development during CreateConsoleEnvironment + // but we explicitly set it here as well to ensure ValidateScopes and ValidateOnBuild are enabled. this.hostBuilder.UseEnvironment("Development"); this.hostBuilder.Build(); - // If build was successful and did not throw an exception, return a console that does nothing and then terminates. - return new NoOperationConsole(); + // If build was successful and did not throw an exception, return a console that logs a success message and then terminates. + return new CheckDepsConsole(); } var host = this.hostBuilder.Build(); @@ -113,13 +115,19 @@ internal static DotNetConsoleBuilder CreateBuilderInternal(Assembly assembly, Ty var environment = DotNetConsoleDefaults.CreateConsoleEnvironment(args); var configuration = DotNetConsoleDefaults.CreateConsoleConfiguration(assembly, args, environment); - // Create a HostBuilder + // Initialize NLog logger from configuration with fallback; any config errors are handled in Initialize + DotNetConsoleLogger.Initialize(configuration); + + // Create a HostBuilder and configure logging and services var builder = Host.CreateDefaultBuilder(args) .UseContentRoot(environment.ContentRootPath) - .ConfigureLogging((context, logging) => + .ConfigureLogging((_, logging) => { + // Remove default providers and add core providers for debug and event sources AdjustDefaultBuilderLoggingProviders(logging); - logging.AddNLog(context.Configuration); + + // Add NLog provider using existing LogManager configuration + logging.AddNLog(); }) .ConfigureServices((_, services) => { @@ -137,7 +145,8 @@ internal static DotNetConsoleBuilder CreateBuilderInternal(Assembly assembly, Ty var parsedArguments = Parser.Default.ParseArguments(args, verbTypes); var consoleBuilder = new DotNetConsoleBuilder(builder, parsedArguments, environment, configuration); - if (args.Length == 1 && string.Equals(args[0], "check-deps", StringComparison.OrdinalIgnoreCase)) + // Determine if this is a check-deps run: only DI validation should run + if (DotNetConsoleDefaults.IsCheckDependenciesRun(args)) { consoleBuilder.checkDependencies = true; return consoleBuilder; diff --git a/Neolution.DotNet.Console/DotNetConsoleDefaults.cs b/Neolution.DotNet.Console/DotNetConsoleDefaults.cs index 6744df4..b4dd50a 100644 --- a/Neolution.DotNet.Console/DotNetConsoleDefaults.cs +++ b/Neolution.DotNet.Console/DotNetConsoleDefaults.cs @@ -12,6 +12,21 @@ /// internal static class DotNetConsoleDefaults { + /// + /// The command argument used to trigger dependency validation. + /// + internal const string CheckDependenciesCommand = "check-deps"; + + /// + /// Determines if the given arguments represent a check-deps run. + /// + /// The command line arguments. + /// True if this is a check-deps run, false otherwise. + public static bool IsCheckDependenciesRun(string[] args) + { + return args.Length == 1 && string.Equals(args[0], CheckDependenciesCommand, StringComparison.OrdinalIgnoreCase); + } + /// /// Creates the console environment. /// @@ -27,9 +42,17 @@ internal static DotNetConsoleEnvironment CreateConsoleEnvironment(string[] args) // The apps root directory is where the appsettings.json are located var appRootDirectory = AppContext.BaseDirectory; + // Check if this is a check-deps run - if so, always use Development environment + var isCheckDepsRun = IsCheckDependenciesRun(args); + + // Default to Production for normal runs, matching ASP.NET Core behavior + // For check-deps, always use Development to ensure appsettings.Development.json is loaded + // Environment can be overridden via DOTNET_ENVIRONMENT or command line arguments + var defaultEnvironment = isCheckDepsRun ? Environments.Development : Environments.Production; + return new DotNetConsoleEnvironment { - EnvironmentName = configuration[HostDefaults.EnvironmentKey] ?? Environments.Production, + EnvironmentName = isCheckDepsRun ? Environments.Development : (configuration[HostDefaults.EnvironmentKey] ?? defaultEnvironment), ApplicationName = AppDomain.CurrentDomain.FriendlyName, ContentRootPath = appRootDirectory, ContentRootFileProvider = new PhysicalFileProvider(appRootDirectory), diff --git a/Neolution.DotNet.Console/DotNetConsoleLogger.cs b/Neolution.DotNet.Console/DotNetConsoleLogger.cs index 5692252..c579472 100644 --- a/Neolution.DotNet.Console/DotNetConsoleLogger.cs +++ b/Neolution.DotNet.Console/DotNetConsoleLogger.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Configuration; using NLog; using NLog.Extensions.Logging; - using NLog.Targets; /// /// Provides static methods to initialize and manage a logger instance. @@ -26,47 +25,39 @@ public static Logger Log { if (logger == null) { - throw new InvalidOperationException("Logger has not been initialized. Call Initialize(configuration) first."); + throw new InvalidOperationException("Logger has not yet been initialized."); } return logger; } } + /// + /// Ensures the logger flushes messages and shuts down internal timers. + /// + public static void Shutdown() + { + LogManager.Shutdown(); + } + /// /// Initializes the logger based on the provided configuration. /// /// The configuration used to initialize the logger. - public static void Initialize(IConfiguration configuration) + internal static void Initialize(IConfiguration configuration) { - ConsoleTarget? consoleTarget = null; try { - logger = LogManager.Setup().LoadConfigurationFromSection(configuration).GetCurrentClassLogger(); + LogManager.Setup().LoadConfigurationFromSection(configuration); + logger = LogManager.GetCurrentClassLogger(); } catch (Exception ex) { - // Create a simple NLog configuration that logs to the console - var config = new NLog.Config.LoggingConfiguration(); - consoleTarget = new ConsoleTarget("console"); - config.AddRule(LogLevel.Trace, LogLevel.Fatal, consoleTarget); - - LogManager.Configuration = config; + // Fallback: minimal console logger setup using NLog fluent API + LogManager.Setup().LoadConfiguration(builder => builder.ForLogger().WriteToConsole()); logger = LogManager.GetCurrentClassLogger(); - logger.Error(ex, "Logger initialization failed"); + logger.Error(ex, "Logger initialization failed."); } - finally - { - consoleTarget?.Dispose(); - } - } - - /// - /// Ensures the logger flushes messages and shuts down internal timers. - /// - public static void Shutdown() - { - LogManager.Shutdown(); } } } diff --git a/Neolution.DotNet.Console/Internal/NoOperationConsole.cs b/Neolution.DotNet.Console/Internal/CheckDepsConsole.cs similarity index 61% rename from Neolution.DotNet.Console/Internal/NoOperationConsole.cs rename to Neolution.DotNet.Console/Internal/CheckDepsConsole.cs index 2e43ace..f65f1ec 100644 --- a/Neolution.DotNet.Console/Internal/NoOperationConsole.cs +++ b/Neolution.DotNet.Console/Internal/CheckDepsConsole.cs @@ -1,15 +1,16 @@ namespace Neolution.DotNet.Console.Internal { using System; + using System.Globalization; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Neolution.DotNet.Console.Abstractions; /// - /// The no operation console application. + /// The check dependencies console application. /// /// - public class NoOperationConsole : IDotNetConsole + internal class CheckDepsConsole : IDotNetConsole { /// public IServiceProvider Services => new ServiceCollection().BuildServiceProvider(); @@ -17,6 +18,7 @@ public class NoOperationConsole : IDotNetConsole /// public Task RunAsync() { + DotNetConsoleLogger.Log.Info(CultureInfo.InvariantCulture, message: "Dependency injection validation succeeded. All registered services can be constructed and no DI issues were found."); return Task.CompletedTask; } } diff --git a/Neolution.DotNet.Console/Properties/AssemblyInfo.cs b/Neolution.DotNet.Console/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..44efd0d --- /dev/null +++ b/Neolution.DotNet.Console/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Neolution.DotNet.Console.UnitTests")]