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
2 changes: 1 addition & 1 deletion .docfx/Dockerfile.docfx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG NGINX_VERSION=1.30.0-alpine
ARG NGINX_VERSION=1.31.0-alpine

FROM --platform=$BUILDPLATFORM nginx:${NGINX_VERSION} AS base
RUN rm -rf /usr/share/nginx/html/*
Expand Down
22 changes: 22 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,28 @@ Internal classes and methods must be validated by exercising the public API that
- Public entry points provide sufficient coverage of internal code paths.
- The internal implementation exists solely as a helper or utility for public-facing functionality.

## 10. ExcludeFromCodeCoverage Prohibition

**Do not use `ExcludeFromCodeCoverage` attribute on any code.** This includes:

- Test classes or test methods
- Production code
- Configuration code
- Any other code path

### Rationale

- Excluding code from coverage hides gaps and creates false confidence in test completeness.
- If a code path cannot or should not be tested, refactor the code to eliminate that path rather than hiding it from metrics.
- Every executable line should be covered by tests or be genuinely unreachable (dead code to be removed).

### Alternative Approaches

- **Untestable code paths**: Refactor to separate concerns and eliminate the untestable path.
- **External dependencies**: Use test doubles (fakes, stubs, spies) instead of excluding from coverage.
- **Configuration-only code**: Move to configuration files or extract into testable methods.
- **Generated or third-party code**: These should not be in the primary codebase; use NuGet packages or dedicated vendor folders if necessary.

---
description: 'Writing Performance Tests'
applyTo: "tuning/**, **/*Benchmark*.cs"
Expand Down
89 changes: 82 additions & 7 deletions .github/workflows/ci-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ on:
options:
- Debug
- Release
run_mac_tests:
type: boolean
description: Run the macOS test matrix despite the additional cost and runtime.
default: false

permissions:
contents: read
Expand All @@ -21,6 +25,7 @@ jobs:
name: initialize
runs-on: ubuntu-24.04
outputs:
run-mac-tests: ${{ steps.vars.outputs.run-mac-tests }}
run-privileged-jobs: ${{ steps.vars.outputs.run-privileged-jobs }}
strong-name-key-filename: ${{ steps.vars.outputs.strong-name-key-filename }}
build-switches: ${{ steps.vars.outputs.build-switches }}
Expand All @@ -29,6 +34,12 @@ jobs:
name: calculate workflow variables
shell: bash
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.run_mac_tests }}" == "true" ]]; then
echo "run-mac-tests=true" >> "$GITHUB_OUTPUT"
else
echo "run-mac-tests=false" >> "$GITHUB_OUTPUT"
fi

if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then
echo "run-privileged-jobs=false" >> "$GITHUB_OUTPUT"
echo "strong-name-key-filename=" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -108,10 +119,74 @@ jobs:
build: true
download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }}

test_mac:
if: ${{ needs.init.outputs.run-mac-tests == 'true' }}
name: call-test-mac
needs: [init, build]
strategy:
fail-fast: false
matrix:
arch: [X64, ARM64]
configuration: [Debug, Release]
uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3
with:
runs-on: ${{ matrix.arch == 'ARM64' && 'macos-26' || 'macos-26-intel' }}
projects: test/**/*.csproj
configuration: ${{ matrix.configuration }}
verbosity-level: normal
build-switches: -p:SkipSignAssembly=true
restore: true
build: true
download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }}

test_qualitygate:
if: ${{ always() }}
name: test-qualitygate
needs: [init, test_linux, test_windows, test_mac]
runs-on: ubuntu-24.04
steps:
- name: Evaluate test results
shell: bash
env:
RUN_MAC_TESTS: ${{ needs.init.outputs.run-mac-tests }}
TEST_LINUX_RESULT: ${{ needs.test_linux.result }}
TEST_WINDOWS_RESULT: ${{ needs.test_windows.result }}
TEST_MAC_RESULT: ${{ needs.test_mac.result }}
run: |
require_success() {
local job_name="$1"
local job_result="$2"

if [[ "$job_result" != "success" ]]; then
echo "::error::$job_name finished with '$job_result'."
exit 1
fi
}

require_success_or_skip() {
local job_name="$1"
local job_enabled="$2"
local job_result="$3"

if [[ "$job_enabled" == "true" ]]; then
require_success "$job_name" "$job_result"
return
fi

if [[ "$job_result" != "success" && "$job_result" != "skipped" ]]; then
echo "::error::$job_name finished with '$job_result' while disabled."
exit 1
fi
}

require_success "test_linux" "$TEST_LINUX_RESULT"
require_success "test_windows" "$TEST_WINDOWS_RESULT"
require_success_or_skip "test_mac" "$RUN_MAC_TESTS" "$TEST_MAC_RESULT"

sonarcloud:
if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }}
if: ${{ always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success' }}
name: call-sonarcloud
needs: [init, build, test_linux, test_windows]
needs: [init, build, test_qualitygate]
uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3
with:
organization: geekle
Expand All @@ -120,26 +195,26 @@ jobs:
secrets: inherit

codecov:
if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }}
if: ${{ always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success' }}
name: call-codecov
needs: [init, build, test_linux, test_windows]
needs: [init, build, test_qualitygate]
uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1
with:
repository: codebeltnet/bootstrapper
secrets: inherit

codeql:
if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }}
if: ${{ always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success' }}
name: call-codeql
needs: [init, build, test_linux, test_windows]
needs: [init, build, test_qualitygate]
uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v3
permissions:
security-events: write

deploy:
if: github.event_name != 'pull_request'
name: call-nuget
needs: [build, pack, test_linux, test_windows, sonarcloud, codecov, codeql]
needs: [build, pack, test_qualitygate, sonarcloud, codecov, codeql]
uses: codebeltnet/jobs-nuget-push/.github/workflows/default.yml@v3
with:
version: ${{ needs.build.outputs.version }}
Expand Down
6 changes: 6 additions & 0 deletions .nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Version: 5.1.0
Availability: .NET 10 and .NET 9

# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)

Version: 5.0.7
Availability: .NET 10 and .NET 9

Expand Down
6 changes: 6 additions & 0 deletions .nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Version: 5.1.0
Availability: .NET 10 and .NET 9

# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)

Version: 5.0.7
Availability: .NET 10 and .NET 9

Expand Down
6 changes: 6 additions & 0 deletions .nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Version: 5.1.0
Availability: .NET 10 and .NET 9

# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)

Version: 5.0.7
Availability: .NET 10 and .NET 9

Expand Down
10 changes: 10 additions & 0 deletions .nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
Version: 5.1.0
Availability: .NET 10 and .NET 9

# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)

# New Features
- ADDED UseBootstrapperEnvironmentDefaults extension method on HostApplicationBuilderExtensions to add conventional environment defaults and user secrets for local development,
- ADDED UseBootstrapperEnvironmentDefaults extension method on HostBuilderExtensions to add user secrets for local development.

Version: 5.0.7
Availability: .NET 10 and .NET 9

Expand Down
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder.

## [5.1.0] - 2026-05-28

This is a minor release focused on environment configuration defaults for local development, expanded test coverage to 95%, dependency updates, and CI/CD improvements for macOS testing.

### Added

- UseBootstrapperEnvironmentDefaults extension method on IHostApplicationBuilder to add conventional environment defaults and user secrets for local development,
- UseBootstrapperEnvironmentDefaults extension method on IHostBuilder to add user secrets for local development,
- Comprehensive test coverage across bootstrapper console, web, and worker modules including program creation, startup configuration, hosted service initialization, and lifetime management.

### Changed

- Microsoft.NET.Test.Sdk bumped from 18.4.0 to 18.6.0,
- coverlet.collector and coverlet.msbuild bumped from 10.0.0 to 10.0.1,
- Target framework packages updated to latest available versions for net9 and net10,
- DocFX base image updated from nginx 1.30.0-alpine to 1.31.0-alpine,
- CI pipeline enhanced with optional macOS test matrix for X64 and ARM64 architectures across Debug and Release configurations.

## [5.0.7] - 2026-04-18

This is a service update that focuses on package dependencies.
Expand Down Expand Up @@ -226,7 +244,9 @@ Highlighted features included in this release:
- WorkerProgram class in the Codebelt.Bootstrapper.Worker namespace that is the base entry point of an application responsible for registering its WorkerStartup partner
- WorkerStartup interface in the Codebelt.Bootstrapper.Worker namespace that provides the base class of a conventional based Startup class for a console application

[Unreleased]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.6...HEAD
[Unreleased]: https://github.com/codebeltnet/bootstrapper/compare/v5.1.0...HEAD
[5.1.0]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.7...v5.1.0
[5.0.7]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.6...v5.0.7
[5.0.6]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.5...v5.0.6
[5.0.5]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.4...v5.0.5
[5.0.4]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.3...v5.0.4
Expand Down
34 changes: 17 additions & 17 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,30 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Codebelt.Extensions.Swashbuckle.AspNetCore" Version="10.2.0" />
<PackageVersion Include="Codebelt.Extensions.Xunit.App" Version="11.0.9" />
<PackageVersion Include="Cuemon.Core" Version="10.5.1" />
<PackageVersion Include="Cuemon.Extensions.Hosting" Version="10.5.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageVersion Include="Codebelt.Extensions.Swashbuckle.AspNetCore" Version="10.2.1" />
<PackageVersion Include="Codebelt.Extensions.Xunit.App" Version="11.0.10" />
<PackageVersion Include="Cuemon.Core" Version="10.5.2" />
<PackageVersion Include="Cuemon.Extensions.Hosting" Version="10.5.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageVersion Include="MinVer" Version="7.0.0" />
<PackageVersion Include="coverlet.collector" Version="10.0.0" />
<PackageVersion Include="coverlet.msbuild" Version="10.0.0" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="coverlet.msbuild" Version="10.0.1" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="xunit.v3.runner.console" Version="3.2.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith('net9'))">
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.15" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.15" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.15" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.15" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.15" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.16" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.16" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.16" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.16" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.16" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith('net10'))">
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.6" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.6" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.8" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions src/Codebelt.Bootstrapper.Console/ConsoleProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ protected static IHostBuilder CreateHostBuilder(string[] args)
return Host.CreateDefaultBuilder(args)
.UseBootstrapperLifetime()
.UseBootstrapperStartup<TStartup>()
.UseBootstrapperEnvironmentDefaults<TStartup>()
.UseConsoleStartup<TStartup>();
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Codebelt.Bootstrapper.Console/MinimalConsoleProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal static HostApplicationBuilder CreateHostBuilder(string[] args, Type pro
{
var hb = Host.CreateApplicationBuilder(args);
hb.UseBootstrapperLifetime();
hb.UseBootstrapperEnvironmentDefaults();
hb.UseBootstrapperProgram(programType);
hb.UseMinimalConsoleProgram();
return hb;
Expand Down
1 change: 1 addition & 0 deletions src/Codebelt.Bootstrapper.Web/MinimalWebProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ protected static WebApplicationBuilder CreateHostBuilder(string[] args)
{
var hb = WebApplication.CreateBuilder(args);
hb.UseBootstrapperLifetime();
hb.UseBootstrapperEnvironmentDefaults();
return hb;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Codebelt.Bootstrapper.Web/WebProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ protected static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseBootstrapperLifetime()
.UseBootstrapperEnvironmentDefaults<TStartup>()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<TStartup>();
Expand Down
1 change: 1 addition & 0 deletions src/Codebelt.Bootstrapper.Worker/MinimalWorkerProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ protected static HostApplicationBuilder CreateHostBuilder(string[] args)
{
var hb = Host.CreateApplicationBuilder(args);
hb.UseBootstrapperLifetime();
hb.UseBootstrapperEnvironmentDefaults();
return hb;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Codebelt.Bootstrapper.Worker/WorkerProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ protected static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseBootstrapperLifetime()
.UseBootstrapperEnvironmentDefaults<TStartup>()
.UseBootstrapperStartup<TStartup>();
}
}
Expand Down
35 changes: 33 additions & 2 deletions src/Codebelt.Bootstrapper/HostApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Internal;
using System.IO;
using System.Reflection;
using Microsoft.Extensions.Configuration;

namespace Codebelt.Bootstrapper
{
Expand All @@ -22,5 +25,33 @@ public static IHostApplicationBuilder UseBootstrapperLifetime(this IHostApplicat
hostBuilder.Services.AddSingleton<IHostLifetimeEvents>(provider => provider.GetRequiredService<IHostLifetime>() as BootstrapperLifetime);
return hostBuilder;
}

/// <summary>
/// Adds conventional environment defaults for local development.
/// </summary>
/// <param name="hostBuilder">The <see cref="IHostApplicationBuilder" /> to configure.</param>
/// <returns>The same instance of the <see cref="IHostApplicationBuilder"/> for chaining.</returns>
/// <remarks>
/// When the current environment is local development, this method attempts to resolve the application assembly from <see cref="IHostEnvironment.ApplicationName"/>
/// and adds user secrets to <see cref="IConfigurationBuilder"/>. If the application assembly cannot be resolved, the operation is ignored.
/// </remarks>
public static IHostApplicationBuilder UseBootstrapperEnvironmentDefaults(this IHostApplicationBuilder hostBuilder)
{
if (!hostBuilder.Environment.IsLocalDevelopment()) { return hostBuilder; }
if (hostBuilder.Environment.ApplicationName is not { Length: > 0 } applicationName) { return hostBuilder; }
var reloadOnChange = hostBuilder.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);

try
{
var appAssembly = Assembly.Load(new AssemblyName(applicationName));
hostBuilder.Configuration.AddUserSecrets(appAssembly, optional: true, reloadOnChange: reloadOnChange);
}
catch (FileNotFoundException)
{
// The assembly cannot be found, so just skip it.
}

return hostBuilder;
}
}
}
Loading
Loading