From dba2d5534df56ba58190be16c9df3da2630ee8b8 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Fri, 11 Sep 2026 20:43:15 +0200 Subject: [PATCH 01/11] =?UTF-8?q?=F0=9F=93=9A=20add=20cuemon.extensions.fi?= =?UTF-8?q?leproviders.physical=20to=20nuget=20packages=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1f9d9cb37..fa415616a 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ Provides a focused API for building various types of .NET projects. | [Cuemon.Extensions.Data.Integrity](https://www.nuget.org/packages/Cuemon.Extensions.Data.Integrity/) | ![vNext](https://img.shields.io/nuget/vpre/Cuemon.Extensions.Data.Integrity?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Cuemon.Extensions.Data.Integrity?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Cuemon.Extensions.Data.Integrity?color=blueviolet&logo=nuget) | | [Cuemon.Extensions.DependencyInjection](https://www.nuget.org/packages/Cuemon.Extensions.DependencyInjection/) | ![vNext](https://img.shields.io/nuget/vpre/Cuemon.Extensions.DependencyInjection?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Cuemon.Extensions.DependencyInjection?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Cuemon.Extensions.DependencyInjection?color=blueviolet&logo=nuget) | | [Cuemon.Extensions.Diagnostics](https://www.nuget.org/packages/Cuemon.Extensions.Diagnostics/) | ![vNext](https://img.shields.io/nuget/vpre/Cuemon.Extensions.Diagnostics?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Cuemon.Extensions.Diagnostics?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Cuemon.Extensions.Diagnostics?color=blueviolet&logo=nuget) | +| [Cuemon.Extensions.FileProviders.Physical](https://www.nuget.org/packages/Cuemon.Extensions.FileProviders.Physical/) | ![vNext](https://img.shields.io/nuget/vpre/Cuemon.Extensions.FileProviders.Physical?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Cuemon.Extensions.FileProviders.Physical?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Cuemon.Extensions.FileProviders.Physical?color=blueviolet&logo=nuget) | | [Cuemon.Extensions.Hosting](https://www.nuget.org/packages/Cuemon.Extensions.Hosting/) | ![vNext](https://img.shields.io/nuget/vpre/Cuemon.Extensions.Hosting?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Cuemon.Extensions.Hosting?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Cuemon.Extensions.Hosting?color=blueviolet&logo=nuget) | | [Cuemon.Extensions.IO](https://www.nuget.org/packages/Cuemon.Extensions.IO/) | ![vNext](https://img.shields.io/nuget/vpre/Cuemon.Extensions.IO?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Cuemon.Extensions.IO?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Cuemon.Extensions.IO?color=blueviolet&logo=nuget) | | [Cuemon.Extensions.Net](https://www.nuget.org/packages/Cuemon.Extensions.Net/) | ![vNext](https://img.shields.io/nuget/vpre/Cuemon.Extensions.Net?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Cuemon.Extensions.Net?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Cuemon.Extensions.Net?color=blueviolet&logo=nuget) | From f44b4b620cb1a5efec1f6073d9d0394c49aaef98 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Thu, 17 Sep 2026 00:13:31 +0200 Subject: [PATCH 02/11] =?UTF-8?q?=F0=9F=94=A8=20optimize=20docfx=20metadat?= =?UTF-8?q?a=20build=20process?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add conditional --noRestore flag to improve DocFX metadata generation performance by detecting when restore operations are unnecessary based on NuGet cache and dependency staleness. --- .docfx/BuildDocfxImage.ps1 | 103 ++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/.docfx/BuildDocfxImage.ps1 b/.docfx/BuildDocfxImage.ps1 index 65016d084..b0b7b3f49 100644 --- a/.docfx/BuildDocfxImage.ps1 +++ b/.docfx/BuildDocfxImage.ps1 @@ -1,4 +1,105 @@ $version = minver -i -t v -v w -docfx metadata docfx.json +$docfxRoot = (Get-Location).Path +$sourceRoot = [System.IO.Path]::GetFullPath((Join-Path $docfxRoot '..\src')) +$docfxConfig = Get-Content -Raw 'docfx.json' | ConvertFrom-Json +$metadataProjectPatterns = @( + foreach ($metadata in $docfxConfig.metadata) { + foreach ($source in $metadata.src) { + $metadataSourceRoot = if ($source.src) { + [System.IO.Path]::GetFullPath((Join-Path $docfxRoot $source.src)) + } else { + $docfxRoot + } + + foreach ($file in $source.files) { + (Join-Path $metadataSourceRoot $file).Replace('/', [System.IO.Path]::DirectorySeparatorChar) + } + } + } +) +$sourceProjects = @(Get-ChildItem -LiteralPath $sourceRoot -Recurse -File -Filter '*.csproj') +$metadataProjects = @( + $sourceProjects | + Where-Object { + $projectPath = $_.FullName + foreach ($pattern in $metadataProjectPatterns) { + if ($projectPath -like $pattern) { + return $true + } + } + + return $false + } +) +$sourceProjectsHaveRestoreAssets = $sourceProjects.Count -gt 0 +foreach ($project in $sourceProjects) { + $restoreAssetsPath = Join-Path $project.DirectoryName 'obj\project.assets.json' + if (-not (Test-Path -LiteralPath $restoreAssetsPath -PathType Leaf)) { + $sourceProjectsHaveRestoreAssets = $false + break + } +} + +$useNoRestore = $metadataProjects.Count -gt 0 -and $sourceProjectsHaveRestoreAssets +$restoreInputNames = @('Directory.Build.props', 'Directory.Build.targets', 'Directory.Packages.props', 'NuGet.Config', 'nuget.config', 'global.json') + +foreach ($project in $metadataProjects) { + $restoreAssetsPath = Join-Path $project.DirectoryName 'obj\project.assets.json' + if (-not (Test-Path -LiteralPath $restoreAssetsPath -PathType Leaf)) { + $useNoRestore = $false + break + } + + $restoreAssetsLastWriteTime = (Get-Item -LiteralPath $restoreAssetsPath).LastWriteTimeUtc + $restoreInputPaths = [System.Collections.Generic.List[string]]::new() + $restoreInputPaths.Add($project.FullName) + + $inputDirectory = $project.DirectoryName + while ($inputDirectory) { + foreach ($name in $restoreInputNames) { + $restoreInputPath = Join-Path $inputDirectory $name + if (Test-Path -LiteralPath $restoreInputPath -PathType Leaf) { + $restoreInputPaths.Add($restoreInputPath) + } + } + + $parentDirectory = Split-Path -Parent $inputDirectory + if (-not $parentDirectory -or $parentDirectory -eq $inputDirectory) { + break + } + + $inputDirectory = $parentDirectory + } + + $lockFilePath = Join-Path $project.DirectoryName 'packages.lock.json' + if (Test-Path -LiteralPath $lockFilePath -PathType Leaf) { + $restoreInputPaths.Add($lockFilePath) + } + + if ($env:APPDATA) { + $userNuGetConfigPath = Join-Path $env:APPDATA 'NuGet\NuGet.Config' + if (Test-Path -LiteralPath $userNuGetConfigPath -PathType Leaf) { + $restoreInputPaths.Add($userNuGetConfigPath) + } + } + + foreach ($restoreInputPath in $restoreInputPaths | Sort-Object -Unique) { + if ((Get-Item -LiteralPath $restoreInputPath).LastWriteTimeUtc -gt $restoreAssetsLastWriteTime) { + $useNoRestore = $false + break + } + } + + if (-not $useNoRestore) { + break + } +} + +# Keep the metadata groups in one process; DocFX carries resolver state across groups. +if ($useNoRestore) { + docfx metadata docfx.json --noRestore +} else { + docfx metadata docfx.json +} docker buildx build -t cuemon-docfx:$version --platform linux/arm64,linux/amd64 --load -f Dockerfile.docfx . get-childItem -recurse -path api -include *.yml, .manifest | remove-item From 40d879b9ed0fa648a9ee615ec494afbf6891cc74 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Thu, 17 Sep 2026 00:13:37 +0200 Subject: [PATCH 03/11] =?UTF-8?q?=F0=9F=94=A7=20update=20documentation=20c?= =?UTF-8?q?onfiguration=20and=20structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add memberLayout separatePages to all metadata groups for improved documentation organization, update git branch reference to main, add ms-style template, remove outdated Other Projects navigation, and delete the superseded extensions index overview page. --- .docfx/api/extensions/index.md | 76 ---------------------------------- .docfx/docfx.json | 9 +++- .docfx/toc.yml | 4 -- 3 files changed, 7 insertions(+), 82 deletions(-) delete mode 100644 .docfx/api/extensions/index.md diff --git a/.docfx/api/extensions/index.md b/.docfx/api/extensions/index.md deleted file mode 100644 index 3f2a5e14b..000000000 --- a/.docfx/api/extensions/index.md +++ /dev/null @@ -1,76 +0,0 @@ ---- -uid: other-projects-md -title: Other Projects ---- - -# Other Projects - -Here is a collection of various non-Microsoft APIs that was adapted by Cuemon for .NET to provide an abundance of enhancements, new features and extension methods. - -## xUnit API - -[xUnit.net](https://xunit.net/) has from day one been the preferred unit test platform for Cuemon for .NET and it was only natural to extend upon xUnit for even more advanced unit test scenarios. - -[!INCLUDE [availability-hybrid](../../includes/availability-hybrid.md)] - -Complements: [xUnit.net](https://github.com/xunit/xunit) 🔗 - -> **Note** -> Since `Cuemon for .NET` has always been about extending official .NET APIs, this project is no longer maintained as part of the Cuemon assembly family. It has been moved to its own repository and is now called [Extensions for xUnit API by Codebelt](https://github.com/codebeltnet/xunit). - -## Json.NET API - -I am a huge fan of [Json.NET](https://www.newtonsoft.com/json) written by [James Newton-King](https://github.com/JamesNK) and the flexible architecture this JSON framework adds to the toolbelt. - -So even though Microsoft decided to write their own [JSON framework](https://docs.microsoft.com/en-us/dotnet/api/system.text.json) (first seen with the release of ASP.NET Core 3), Cuemon for .NET will continue to support and extend Json.NET as the natural companion of our framework. - -[!INCLUDE [availability-hybrid](../../includes/availability-hybrid.md)] - -Complements: [Json.NET](https://github.com/JamesNK/Newtonsoft.Json) 🔗 - -> **Note** -> Since `Cuemon for .NET` has always been about extending official .NET APIs, this project is no longer maintained as part of the Cuemon assembly family. It has been moved to its own repository and is now called [Extensions for Newtonsoft.Json API by Codebelt](https://github.com/codebeltnet/newtonsoft-json). - -## Asp.Versioning API - -Our preferred way of versioning Open API/Swagger for RESTful APIs is done through [Asp.Versioning](https://github.com/dotnet/aspnet-api-versioning). - -[!INCLUDE [availability-modern](../../includes/availability-modern.md)] - -Complements: [Asp.Versioning](https://github.com/dotnet/aspnet-api-versioning) 🔗 - -> **Note** -> Since `Cuemon for .NET` has always been about extending official .NET APIs, this project is no longer maintained as part of the Cuemon assembly family. It has been moved to its own repository and is now called [Extensions for Asp.Versioning API by Codebelt](https://github.com/codebeltnet/asp-versioning). - -## Swashbuckle.AspNetCore API - -[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) defined the standard for Open API/Swagger which we built upon to provide an even more powerful and efficient way of documenting your RESTful APIs. - -[!INCLUDE [availability-modern](../../includes/availability-modern.md)] - -Complements: [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) 🔗 - -> **Note** -> Since `Cuemon for .NET` has always been about extending official .NET APIs, this project is no longer maintained as part of the Cuemon assembly family. It has been moved to its own repository and is now called [Extensions for Swashbuckle.AspNetCore API by Codebelt](https://github.com/codebeltnet/swashbuckle-aspnetcore). - -## YamlDotNet API - -[YamlDotNet](https://github.com/aaubry/YamlDotNet) is the most matured YAML library for .NET, why we decided to abandon own efforts to write a YAML library from scratch. That written, we happily built upon `YamlDotNet` to provide an even better developer experience when working with YAML. - -[!INCLUDE [availability-modern](../../includes/availability-modern.md)] - -Complements: [YamlDotNet](https://github.com/aaubry/YamlDotNet/wiki) 🔗 - -> **Note** -> Since `Cuemon for .NET` has always been about extending official .NET APIs, this project is no longer maintained as part of the Cuemon assembly family. It has been moved to its own repository and is now called [Extensions for YamlDotNet API by Codebelt](https://github.com/codebeltnet/yamldotnet). - -## AWS Signature API - -Providing an additional HTTP HMAC Authentication header that provides a fluent way to use [AWS Signature Version 4](https://docs.aws.amazon.com/general/latest/gr/reference-for-signature-version-4.html) was a fun challenge to write and add to Cuemon for .NET . - -[!INCLUDE [availability-modern](../../includes/availability-modern.md)] - -Complements: [Authenticating Requests (AWS Signature Version 4)](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) 🔗 - -> **Note** -> Since `Cuemon for .NET` has always been about extending official .NET APIs, this project is no longer maintained as part of the Cuemon assembly family. It has been moved to its own repository and is now called [Extensions for AWS Signature Version 4 API by Codebelt](https://github.com/codebeltnet/aws-signature-v4). diff --git a/.docfx/docfx.json b/.docfx/docfx.json index f3ca71ffe..46475be64 100644 --- a/.docfx/docfx.json +++ b/.docfx/docfx.json @@ -22,6 +22,7 @@ } ], "dest": "api/dotnet", + "memberLayout": "separatePages", "filter": "filterConfig.yml", "properties": { "TargetFramework": "net10.0" @@ -53,6 +54,7 @@ } ], "dest": "api/extensions/dotnet", + "memberLayout": "separatePages", "filter": "filterConfig.yml", "properties": { "TargetFramework": "net10.0" @@ -71,6 +73,7 @@ } ], "dest": "api/aspnet", + "memberLayout": "separatePages", "filter": "filterConfig.yml", "properties": { "TargetFramework": "net10.0" @@ -93,6 +96,7 @@ } ], "dest": "api/extensions/aspnet", + "memberLayout": "separatePages", "filter": "filterConfig.yml", "properties": { "TargetFramework": "net10.0" @@ -140,7 +144,7 @@ "_disableContribution": false, "_gitContribute": { "repo": "https://github.com/codebeltnet/cuemon", - "branch": "development" + "branch": "main" }, "_gitUrlPattern": "github", "_lang": "en" @@ -151,7 +155,8 @@ "template": [ "default", "modern", - "templates/cuemon" + "templates/cuemon", + "templates/ms-style" ], "overwrite": [ { diff --git a/.docfx/toc.yml b/.docfx/toc.yml index 08bf7441a..89e029319 100644 --- a/.docfx/toc.yml +++ b/.docfx/toc.yml @@ -16,7 +16,3 @@ href: api/aspnet/Cuemon.AspNetCore.html - name: ASP.NET Core API Extensions href: api/extensions/aspnet/Cuemon.Extensions.AspNetCore.Configuration.html -- name: Other Projects - href: api/extensions -- name: NuGet - href: packages From d69ec9de6685e309fef9113f34355bbcba963f6c Mon Sep 17 00:00:00 2001 From: gimlichael Date: Thu, 17 Sep 2026 19:01:47 +0200 Subject: [PATCH 04/11] =?UTF-8?q?=F0=9F=93=9A=20add=20nuget=20packages=20s?= =?UTF-8?q?ection=20to=20documentation=20toc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .docfx/toc.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.docfx/toc.yml b/.docfx/toc.yml index 89e029319..5b8fbcdac 100644 --- a/.docfx/toc.yml +++ b/.docfx/toc.yml @@ -16,3 +16,5 @@ href: api/aspnet/Cuemon.AspNetCore.html - name: ASP.NET Core API Extensions href: api/extensions/aspnet/Cuemon.Extensions.AspNetCore.Configuration.html +- name: NuGet + href: packages From 1453bdbbeb73e72a1dca712662c956bc691e6caf Mon Sep 17 00:00:00 2001 From: gimlichael Date: Tue, 22 Sep 2026 15:41:09 +0200 Subject: [PATCH 05/11] =?UTF-8?q?=F0=9F=A7=B1=20update=20docker=20containe?= =?UTF-8?q?r=20and=20nginx=20web=20server=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update base Docker images to use version tags instead of specific patch versions. Replace hardcoded nginx entrypoint with configurable nginx.conf to support custom cache headers and routing rules for DocFX-generated assets. --- .docfx/Dockerfile.docfx | 7 +++---- .docfx/nginx.conf | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 .docfx/nginx.conf diff --git a/.docfx/Dockerfile.docfx b/.docfx/Dockerfile.docfx index 7d97dde52..c6698efce 100644 --- a/.docfx/Dockerfile.docfx +++ b/.docfx/Dockerfile.docfx @@ -3,15 +3,14 @@ FROM --platform=$BUILDPLATFORM nginx:${NGINX_VERSION} AS base RUN rm -rf /usr/share/nginx/html/* -FROM --platform=$BUILDPLATFORM codebeltnet/docfx:2.78.5 AS build +FROM --platform=$BUILDPLATFORM codebeltnet/docfx:2 AS build ADD [".", "docfx"] RUN cd docfx; \ docfx build -FROM nginx:${NGINX_VERSION} AS final +FROM dhi.io/nginx:${NGINX_VERSION} AS final WORKDIR /usr/share/nginx/html COPY --from=build /build/docfx/wwwroot /usr/share/nginx/html - -ENTRYPOINT ["nginx", "-g", "daemon off;"] +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/.docfx/nginx.conf b/.docfx/nginx.conf new file mode 100644 index 000000000..c95d1b1b7 --- /dev/null +++ b/.docfx/nginx.conf @@ -0,0 +1,31 @@ +server { + listen 8080; + server_name _; + # The ingress terminates TLS; keep redirects from exposing this container port. + absolute_redirect off; + + root /usr/share/nginx/html; + index index.html; + + # DocFX fingerprints these generated dependencies in their filenames. They can be + # cached permanently because changed content is published under a different URL. + location ~ "^/public/.+-[A-Z0-9]{8}(?:-[A-Z0-9]{8})?(?:\.min)?\.(?:(?:css|js)(?:\.map)?|woff2?)$" { + try_files $uri =404; + add_header Cache-Control "public, max-age=31536000, immutable" always; + add_header Vary "Accept-Encoding" always; + } + + # Pages, navigation data, images, and DocFX entry assets keep stable URLs. Do not + # retain them across deployments, and ignore validators cached under an old policy. + location / { + # Nginx derives ETags from modification time and file size. Those values can be + # reused by files copied from different image layers, producing a false 304. + etag off; + if_modified_since off; + try_files $uri $uri/ =404; + add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always; + add_header Expires "0" always; + add_header Pragma "no-cache" always; + add_header Vary "Accept-Encoding" always; + } +} From 3ba242ca6a2f5f344d1e862d8ef49ce88c2b8824 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Tue, 22 Sep 2026 15:41:14 +0200 Subject: [PATCH 06/11] =?UTF-8?q?=F0=9F=94=A7=20add=20sitemap=20configurat?= =?UTF-8?q?ion=20to=20documentation=20generator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable sitemap generation for docs.cuemon.net with monthly changefreq. Improves search engine discoverability of documentation pages. --- .docfx/docfx.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.docfx/docfx.json b/.docfx/docfx.json index 46475be64..cdd061885 100644 --- a/.docfx/docfx.json +++ b/.docfx/docfx.json @@ -175,6 +175,10 @@ "noLangKeyword": false, "keepFileLink": false, "cleanupCacheHistory": false, - "disableGitFeatures": false + "disableGitFeatures": false, + "sitemap": { + "baseUrl": "https://docs.cuemon.net/", + "changefreq": "monthly" + } } } From 5083af736ece6226c46720c60cb8b8f06044c996 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Tue, 22 Sep 2026 15:41:20 +0200 Subject: [PATCH 07/11] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20upgrade=20test=20dep?= =?UTF-8?q?endencies=20and=20package=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace Microsoft.Testing.Extensions.CodeCoverage with Microsoft.Testing.Extensions.HangDump for better test diagnostics. Add Codebelt.Coverlet.MTP for coverage integration. Update all package versions including xunit.v3, Codebelt extension libraries, and framework-specific Microsoft.Extensions packages across net9 and net10 targets. --- Directory.Build.props | 3 ++- Directory.Packages.props | 32 +++++++++++++++++--------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 7604c1d9a..14fb0414f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -91,13 +91,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Directory.Packages.props b/Directory.Packages.props index df200295a..dc165c904 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,18 +7,20 @@ - - - - - - + + + + + + + - - + + + @@ -35,12 +37,12 @@ - - - - - - + + + + + + @@ -51,6 +53,6 @@ - + \ No newline at end of file From 407dc8e3f1be057217ff0102bb42fc637d0e71ad Mon Sep 17 00:00:00 2001 From: gimlichael Date: Tue, 22 Sep 2026 16:03:28 +0200 Subject: [PATCH 08/11] =?UTF-8?q?=F0=9F=94=A8=20enhance=20DocFX=20build=20?= =?UTF-8?q?script=20and=20container=20optimization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor BuildDocfxImage.ps1 with improved architecture including helper functions for source file discovery, fingerprint calculation, and metadata management. Add ForceMetadata parameter to regenerate assets on demand. Add .dockerignore to optimize container builds by excluding local artifacts and temporary files. --- .docfx/.dockerignore | 5 + .docfx/BuildDocfxImage.ps1 | 232 ++++++++++++++++++++++++------------- 2 files changed, 154 insertions(+), 83 deletions(-) create mode 100644 .docfx/.dockerignore diff --git a/.docfx/.dockerignore b/.docfx/.dockerignore new file mode 100644 index 000000000..c063843c1 --- /dev/null +++ b/.docfx/.dockerignore @@ -0,0 +1,5 @@ +# Local outputs and bookkeeping are not inputs to the container's DocFX build. +wwwroot/ +obj/ +bin/ +*.ps1 diff --git a/.docfx/BuildDocfxImage.ps1 b/.docfx/BuildDocfxImage.ps1 index b0b7b3f49..3d88c39da 100644 --- a/.docfx/BuildDocfxImage.ps1 +++ b/.docfx/BuildDocfxImage.ps1 @@ -1,105 +1,171 @@ -$version = minver -i -t v -v w -$docfxRoot = (Get-Location).Path -$sourceRoot = [System.IO.Path]::GetFullPath((Join-Path $docfxRoot '..\src')) -$docfxConfig = Get-Content -Raw 'docfx.json' | ConvertFrom-Json -$metadataProjectPatterns = @( - foreach ($metadata in $docfxConfig.metadata) { - foreach ($source in $metadata.src) { - $metadataSourceRoot = if ($source.src) { - [System.IO.Path]::GetFullPath((Join-Path $docfxRoot $source.src)) - } else { - $docfxRoot - } +[CmdletBinding()] +param( + # Regenerate metadata even when the inputs and generated files are unchanged. + [switch] $ForceMetadata +) - foreach ($file in $source.files) { - (Join-Path $metadataSourceRoot $file).Replace('/', [System.IO.Path]::DirectorySeparatorChar) - } +$ErrorActionPreference = 'Stop' + +function Assert-CommandSucceeded([string] $Command) { + if ($LASTEXITCODE -ne 0) { throw "$Command failed with exit code $LASTEXITCODE." } +} + +function Get-SourceFiles([string] $Directory) { + foreach ($item in Get-ChildItem -LiteralPath $Directory -Force) { + if ($item.PSIsContainer) { + if ($item.Name -notin @('bin', 'obj', '.git')) { Get-SourceFiles $item.FullName } + } else { + $item.FullName } } -) -$sourceProjects = @(Get-ChildItem -LiteralPath $sourceRoot -Recurse -File -Filter '*.csproj') -$metadataProjects = @( - $sourceProjects | - Where-Object { - $projectPath = $_.FullName - foreach ($pattern in $metadataProjectPatterns) { - if ($projectPath -like $pattern) { - return $true - } - } +} - return $false +function Get-Fingerprint([string[]] $Paths, [string[]] $Values = @()) { + $entries = @( + $Values + foreach ($path in $Paths | Sort-Object -Unique) { + if (Test-Path -LiteralPath $path -PathType Leaf) { + "$path=$((Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash)" + } else { + "$path=" + } } -) -$sourceProjectsHaveRestoreAssets = $sourceProjects.Count -gt 0 -foreach ($project in $sourceProjects) { - $restoreAssetsPath = Join-Path $project.DirectoryName 'obj\project.assets.json' - if (-not (Test-Path -LiteralPath $restoreAssetsPath -PathType Leaf)) { - $sourceProjectsHaveRestoreAssets = $false - break + ) + $sha = [System.Security.Cryptography.SHA256]::Create() + try { + [BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes(($entries -join [Environment]::NewLine)))) + } finally { + $sha.Dispose() } } -$useNoRestore = $metadataProjects.Count -gt 0 -and $sourceProjectsHaveRestoreAssets -$restoreInputNames = @('Directory.Build.props', 'Directory.Build.targets', 'Directory.Packages.props', 'NuGet.Config', 'nuget.config', 'global.json') - -foreach ($project in $metadataProjects) { - $restoreAssetsPath = Join-Path $project.DirectoryName 'obj\project.assets.json' - if (-not (Test-Path -LiteralPath $restoreAssetsPath -PathType Leaf)) { - $useNoRestore = $false - break +function Get-MetadataFiles { + foreach ($directory in $metadataDirectories) { + if (Test-Path -LiteralPath $directory) { + Get-ChildItem -LiteralPath $directory -Recurse -Force -File | + Where-Object { $_.Extension -eq '.yml' -or $_.Name -eq '.manifest' } | + Select-Object -ExpandProperty FullName + } } +} - $restoreAssetsLastWriteTime = (Get-Item -LiteralPath $restoreAssetsPath).LastWriteTimeUtc - $restoreInputPaths = [System.Collections.Generic.List[string]]::new() - $restoreInputPaths.Add($project.FullName) +$totalTimer = [Diagnostics.Stopwatch]::StartNew() +Push-Location $PSScriptRoot +try { + $docfxRoot = $PSScriptRoot + $repoRoot = Split-Path -Parent $docfxRoot + $sourceRoot = Join-Path $repoRoot 'src' + $docfxConfig = Get-Content -LiteralPath (Join-Path $docfxRoot 'docfx.json') -Raw | ConvertFrom-Json + $version = minver -i -t v -v w + Assert-CommandSucceeded 'minver' + $docfxVersion = docfx --version + Assert-CommandSucceeded 'docfx --version' + $sdkVersion = dotnet --version + Assert-CommandSucceeded 'dotnet --version' + $revision = git rev-parse HEAD + Assert-CommandSucceeded 'git rev-parse HEAD' + $branch = git rev-parse --abbrev-ref HEAD + Assert-CommandSucceeded 'git rev-parse --abbrev-ref HEAD' - $inputDirectory = $project.DirectoryName - while ($inputDirectory) { - foreach ($name in $restoreInputNames) { - $restoreInputPath = Join-Path $inputDirectory $name - if (Test-Path -LiteralPath $restoreInputPath -PathType Leaf) { - $restoreInputPaths.Add($restoreInputPath) + # Only these generated destinations may be cleaned; preserve authored Markdown. + $apiRoot = [IO.Path]::GetFullPath((Join-Path $docfxRoot 'api')) + [IO.Path]::DirectorySeparatorChar + $metadataDirectories = @( + foreach ($metadata in $docfxConfig.metadata) { + $destination = [IO.Path]::GetFullPath((Join-Path $docfxRoot $metadata.dest)) + if (-not $destination.StartsWith($apiRoot, [StringComparison]::OrdinalIgnoreCase)) { + throw "Metadata destination must be beneath $apiRoot : $destination" } + $destination } - - $parentDirectory = Split-Path -Parent $inputDirectory - if (-not $parentDirectory -or $parentDirectory -eq $inputDirectory) { - break + ) + $sourceFiles = @(Get-SourceFiles $sourceRoot) + $sourceProjects = @($sourceFiles | Where-Object { [IO.Path]::GetExtension($_) -eq '.csproj' }) + $inputPaths = @( + $PSCommandPath + $sourceFiles + Get-ChildItem -LiteralPath $repoRoot -Force -File | Select-Object -ExpandProperty FullName + foreach ($metadata in $docfxConfig.metadata) { + if ($metadata.filter) { Join-Path $docfxRoot $metadata.filter } } - - $inputDirectory = $parentDirectory - } - - $lockFilePath = Join-Path $project.DirectoryName 'packages.lock.json' - if (Test-Path -LiteralPath $lockFilePath -PathType Leaf) { - $restoreInputPaths.Add($lockFilePath) - } - - if ($env:APPDATA) { - $userNuGetConfigPath = Join-Path $env:APPDATA 'NuGet\NuGet.Config' - if (Test-Path -LiteralPath $userNuGetConfigPath -PathType Leaf) { - $restoreInputPaths.Add($userNuGetConfigPath) + $inputDirectory = $repoRoot + while ($inputDirectory) { + foreach ($name in @('Directory.Build.props', 'Directory.Build.targets', 'Directory.Packages.props', 'NuGet.Config', 'global.json')) { + Join-Path $inputDirectory $name + } + $inputDirectory = Split-Path -Parent $inputDirectory + } + if ($env:APPDATA) { Join-Path $env:APPDATA 'NuGet/NuGet.Config' } + foreach ($project in $sourceProjects) { + $objDirectory = Join-Path (Split-Path -Parent $project) 'obj' + Join-Path $objDirectory 'project.assets.json' + Join-Path $objDirectory ((Split-Path -Leaf $project) + '.nuget.g.props') + Join-Path $objDirectory ((Split-Path -Leaf $project) + '.nuget.g.targets') + } + ) + $inputValues = @( + $version; $docfxVersion; $sdkVersion; $revision; $branch + $docfxConfig.metadata | ConvertTo-Json -Depth 100 -Compress + foreach ($name in @('CI', 'EMAIL', 'Configuration', 'MSBuildSDKsPath', 'DOTNET_ROOT', 'NUGET_PACKAGES', 'GITHUB_RUN_NUMBER')) { + "$name=$([Environment]::GetEnvironmentVariable($name))" + } + ) + $cachePath = Join-Path $docfxRoot 'obj/metadata-cache.json' + $inputFingerprint = Get-Fingerprint $inputPaths $inputValues + $metadataFiles = @(Get-MetadataFiles) + $cache = $null + if (Test-Path -LiteralPath $cachePath) { + try { + $cache = Get-Content -LiteralPath $cachePath -Raw | ConvertFrom-Json + } catch { + Write-Warning 'Metadata cache could not be read; regenerating it.' } } + $reuseMetadata = -not $ForceMetadata -and $cache -and + $cache.inputs -eq $inputFingerprint -and $metadataFiles.Count -gt 0 -and + $cache.outputs -eq (Get-Fingerprint $metadataFiles) - foreach ($restoreInputPath in $restoreInputPaths | Sort-Object -Unique) { - if ((Get-Item -LiteralPath $restoreInputPath).LastWriteTimeUtc -gt $restoreAssetsLastWriteTime) { - $useNoRestore = $false - break + if ($reuseMetadata) { + Write-Host 'Metadata unchanged; reusing verified generated files.' + } else { + $metadataTimer = [Diagnostics.Stopwatch]::StartNew() + # One restore graph replaces DocFX's separate restore for each project. + $restoreSolution = Join-Path ([IO.Path]::GetTempPath()) ("cuemon-docfx-{0}.slnx" -f [Guid]::NewGuid()) + try { + $projectsXml = foreach ($project in $sourceProjects) { + ' ' -f [Security.SecurityElement]::Escape($project) + } + @('') + $projectsXml + @('') | Set-Content -LiteralPath $restoreSolution -Encoding utf8 + dotnet restore $restoreSolution --verbosity quiet + Assert-CommandSucceeded 'dotnet restore' + } finally { + if (Test-Path -LiteralPath $restoreSolution) { Remove-Item -LiteralPath $restoreSolution } } - } - if (-not $useNoRestore) { - break + # Invalidate before generation so an interrupted or failed run cannot be reused. + if (Test-Path -LiteralPath $cachePath) { Remove-Item -LiteralPath $cachePath } + foreach ($file in $metadataFiles) { Remove-Item -LiteralPath $file } + $generationFingerprint = Get-Fingerprint $inputPaths $inputValues + # Keep all groups in one process; DocFX carries resolver state across groups. + docfx metadata docfx.json --noRestore + Assert-CommandSucceeded 'docfx metadata' + $metadataFiles = @(Get-MetadataFiles) + if ($metadataFiles.Count -eq 0) { throw 'DocFX did not generate metadata.' } + if ((Get-Fingerprint $inputPaths $inputValues) -ne $generationFingerprint) { + throw 'Metadata inputs changed during generation; rerun the build.' + } + $cache = @{ + inputs = $generationFingerprint + outputs = Get-Fingerprint $metadataFiles + } + New-Item -ItemType Directory -Path (Split-Path -Parent $cachePath) -Force | Out-Null + $cache | ConvertTo-Json | Set-Content -LiteralPath $cachePath -Encoding utf8 + Write-Host ('Metadata and restore: {0:N2}s' -f $metadataTimer.Elapsed.TotalSeconds) } -} -# Keep the metadata groups in one process; DocFX carries resolver state across groups. -if ($useNoRestore) { - docfx metadata docfx.json --noRestore -} else { - docfx metadata docfx.json + $imageTimer = [Diagnostics.Stopwatch]::StartNew() + docker buildx build -t cuemon-docfx:$version --platform linux/arm64,linux/amd64 --load -f Dockerfile.docfx . + Assert-CommandSucceeded 'docker buildx build' + Write-Host ('Docker image: {0:N2}s; total: {1:N2}s' -f $imageTimer.Elapsed.TotalSeconds, $totalTimer.Elapsed.TotalSeconds) +} finally { + Pop-Location } -docker buildx build -t cuemon-docfx:$version --platform linux/arm64,linux/amd64 --load -f Dockerfile.docfx . -get-childItem -recurse -path api -include *.yml, .manifest | remove-item From c96b81bde426243c169c03fdb18cd89d2f0a5122 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Tue, 22 Sep 2026 16:26:02 +0200 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=8E=A8=20enable=20breadcrumb=20navi?= =?UTF-8?q?gation=20in=20documentation=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .docfx/docfx.json | 1 - .docfx/templates/cuemon/layout/_master.tmpl | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.docfx/docfx.json b/.docfx/docfx.json index cdd061885..2ffac3331 100644 --- a/.docfx/docfx.json +++ b/.docfx/docfx.json @@ -139,7 +139,6 @@ "_appLogoPath": "images/50x50.png", "_appFaviconPath": "images/favicon.ico", "_googleAnalyticsTagId": "UA-126254455-1", - "_disableBreadcrumb": true, "_enableSearch": true, "_disableContribution": false, "_gitContribute": { diff --git a/.docfx/templates/cuemon/layout/_master.tmpl b/.docfx/templates/cuemon/layout/_master.tmpl index 336a8d73d..91f3db938 100644 --- a/.docfx/templates/cuemon/layout/_master.tmpl +++ b/.docfx/templates/cuemon/layout/_master.tmpl @@ -97,6 +97,20 @@
+
+ {{^_disableToc}} + + {{/_disableToc}} + + {{^_disableBreadcrumb}} + + {{/_disableBreadcrumb}} +
+
{{!body}}
From 0ba3ae6dd87418772687f145d901b0fad539ae24 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Tue, 22 Sep 2026 16:27:43 +0200 Subject: [PATCH 10/11] =?UTF-8?q?=F0=9F=94=A7=20refactor=20scorecard=20wor?= =?UTF-8?q?kflow=20with=20updated=20actions=20and=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/scorecard.yml | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index aabea97e4..7ef98cad9 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,10 +1,12 @@ -name: Scorecard supply-chain security +name: Scorecard analysis workflow on: - branch_protection_rule: - schedule: - - cron: '45 17 * * 2' push: - branches: [ "main" ] + # Only the default branch is supported. + branches: + - main + schedule: + # Weekly on Saturdays. + - cron: '30 1 * * 6' permissions: read-all @@ -13,30 +15,42 @@ jobs: name: Scorecard analysis runs-on: ubuntu-latest permissions: + # Needed for Code scanning upload security-events: write + # Needed for GitHub OIDC token if publish_results is true id-token: write steps: - name: "Checkout code" - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@v2.4.0 + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 with: results_file: results.sarif results_format: sarif + # Scorecard team runs a weekly scan of public GitHub repos, + # see https://github.com/ossf/scorecard#public-data. + # Setting `publish_results: true` helps us scale by leveraging your workflow to + # extract the results instead of relying on our own infrastructure to run scans. + # And it's free for you! publish_results: true + # Upload the results as artifacts (optional). Commenting out will disable + # uploads of run results in SARIF format to the repository Actions tab. + # https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts - name: "Upload artifact" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: SARIF file path: results.sarif retention-days: 5 + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: results.sarif From 4130f194acce9f1e2bed77836e27c4f4bd4c2bab Mon Sep 17 00:00:00 2001 From: gimlichael Date: Tue, 22 Sep 2026 20:31:03 +0200 Subject: [PATCH 11/11] =?UTF-8?q?=F0=9F=A7=AA=20improve=20file=20watcher?= =?UTF-8?q?=20change=20notification=20reliability=20in=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PortablePhysicalFileProviderTest.cs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/test/Cuemon.Extensions.FileProviders.Physical.Tests/PortablePhysicalFileProviderTest.cs b/test/Cuemon.Extensions.FileProviders.Physical.Tests/PortablePhysicalFileProviderTest.cs index 22a82ca92..244abe21c 100644 --- a/test/Cuemon.Extensions.FileProviders.Physical.Tests/PortablePhysicalFileProviderTest.cs +++ b/test/Cuemon.Extensions.FileProviders.Physical.Tests/PortablePhysicalFileProviderTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -21,6 +22,7 @@ namespace Cuemon.Extensions.FileProviders; public class PortablePhysicalFileProviderTest : Test { private static readonly TimeSpan ChangeNotificationTimeout = TimeSpan.FromSeconds(15); + private static readonly TimeSpan ChangeNotificationRetryInterval = TimeSpan.FromMilliseconds(250); private static readonly FindMatchingEntryDelegate FindMatchingEntry = CreateFindMatchingEntryDelegate(); private static readonly ResolvePathDelegate ResolvePathWithEntries = CreateResolvePathDelegate(); private static readonly ResolveFileInfoDelegate ResolveFileInfoSelection = CreateResolveFileInfoDelegate(); @@ -853,7 +855,7 @@ public async Task Watch_ShouldResolveLiteralDirectoryFilterWithTrailingSeparator AssertEquivalentChangeToken(baseline, token); Assert.False(ReferenceEquals(NullChangeToken.Singleton, token)); - await AssertEquivalentChangeNotificationAsync(baseline, token, () => File.WriteAllText(Path.Combine(directoryPath, "new.txt"), Guid.NewGuid().ToString("N"))); + await AssertEquivalentChangeNotificationAsync(baseline, token, () => File.WriteAllText(Path.Combine(directoryPath, $"{Guid.NewGuid():N}.txt"), Guid.NewGuid().ToString("N"))); } [Fact] @@ -1219,7 +1221,7 @@ private static async Task AssertEquivalentChangeNotificationAsync(IChangeToken e var expectedChanged = WaitForChangeAsync(expected); var actualChanged = WaitForChangeAsync(actual); - changeAction(); + await SignalChangesUntilCompletedAsync(changeAction, expectedChanged, actualChanged).ConfigureAwait(false); var notifications = await Task.WhenAll(expectedChanged, actualChanged).ConfigureAwait(false); @@ -1229,6 +1231,32 @@ private static async Task AssertEquivalentChangeNotificationAsync(IChangeToken e Assert.True(actual.HasChanged); } + private static async Task SignalChangesUntilCompletedAsync(Action changeAction, params Task[] changeTasks) + { + var allChanges = Task.WhenAll(changeTasks); + var timer = Stopwatch.StartNew(); + + // Polling-based watchers can miss the first mutation while the subscription is still priming. + while (timer.Elapsed < ChangeNotificationTimeout && !allChanges.IsCompleted) + { + changeAction(); + + if (allChanges.IsCompleted) + { + return; + } + + var remaining = ChangeNotificationTimeout - timer.Elapsed; + if (remaining <= TimeSpan.Zero) + { + return; + } + + var delay = remaining < ChangeNotificationRetryInterval ? remaining : ChangeNotificationRetryInterval; + await Task.WhenAny(allChanges, Task.Delay(delay)).ConfigureAwait(false); + } + } + private static async Task WaitForChangeAsync(IChangeToken token) { var changed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);