From 9950be21f555e14be9945ac97c6b277ee97011f8 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Wed, 18 Feb 2026 17:45:09 +0100 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9C=A8=20add=20main.js=20for=20context?= =?UTF-8?q?7=20widget=20integration=20in=20modern=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .docfx/docfx.json | 3 ++- .docfx/templates/modern/public/main.js | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 .docfx/templates/modern/public/main.js diff --git a/.docfx/docfx.json b/.docfx/docfx.json index a7ad870..f714667 100644 --- a/.docfx/docfx.json +++ b/.docfx/docfx.json @@ -64,7 +64,8 @@ "fileMetadataFiles": [], "template": [ "default", - "modern" + "modern", + "templates/modern" ], "overwrite": [ { diff --git a/.docfx/templates/modern/public/main.js b/.docfx/templates/modern/public/main.js new file mode 100644 index 0000000..6f678c6 --- /dev/null +++ b/.docfx/templates/modern/public/main.js @@ -0,0 +1,12 @@ +export default { + start: () => { + const script = document.createElement('script'); + script.src = 'https://context7.com/widget.js'; + script.setAttribute('data-library', '/codebeltnet/xunit'); + script.setAttribute('data-color', '#059669'); + script.setAttribute('data-position', 'bottom-right'); + script.setAttribute('data-placeholder', 'Ask about Codebelt.Extensions.Xunit...'); + script.async = true; + document.body.appendChild(script); + } +}; From 31ad629fa99bdeeadcedb151af632b1ed6b55f97 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Wed, 18 Feb 2026 17:56:28 +0100 Subject: [PATCH 02/10] =?UTF-8?q?=E2=9C=A8=20add=20.editorconfig=20for=20c?= =?UTF-8?q?onsistent=20coding=20styles=20and=20rules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..df96769 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,193 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# Default: prefer spaces for data/markup +indent_style = space +indent_size = 2 +tab_width = 2 + +# This style rule concern the use of the range operator, which is available in C# 8.0 and later. +# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0057 +[*.{cs,vb}] +dotnet_diagnostic.IDE0057.severity = none + +# This style rule concerns the use of switch expressions versus switch statements. +# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0066 +[*.{cs,vb}] +dotnet_diagnostic.IDE0066.severity = none + +# Performance rules +# https://docs.microsoft.com/da-dk/dotnet/fundamentals/code-analysis/quality-rules/performance-warnings +[*.{cs,vb}] +dotnet_analyzer_diagnostic.category-Performance.severity = none # Because many of the suggestions by performance analyzers are not compatible with .NET Standard 2.0 + +# This style rule concerns the use of using statements without curly braces, also known as using declarations. This alternative syntax was introduced in C# 8.0. +# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0063 +[*.{cs,vb}] +dotnet_diagnostic.IDE0063.severity = none + +# This style rule concerns with simplification of interpolated strings to improve code readability. It recommends removal of certain explicit method calls, such as ToString(), when the same method would be implicitly invoked by the compiler if the explicit method call is removed. +# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0071 +[*.{cs,vb}] +dotnet_diagnostic.IDE0071.severity = none + +# S3267: Loops should be simplified with "LINQ" expressions +# https://rules.sonarsource.com/csharp/RSPEC-3267 +dotnet_diagnostic.S3267.severity = none + +# CA1859: Use concrete types when possible for improved performance +# This is a violation of Framework Design Guidelines. +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1859 +[*.{cs,vb}] +dotnet_diagnostic.CA1859.severity = none + +# IDE0008: Use explicit type +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0007-ide0008 +[*.{cs,vb}] +dotnet_diagnostic.IDE0008.severity = none + +[*.{cs,vb}] +indent_style = space +indent_size = 4 + +# IDE0161: Namespace declaration preferences +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0161 +# Prefer file-scoped namespaces for new files; existing block-scoped files should not be converted unless explicitly asked +[*.cs] +csharp_style_namespace_declarations = file_scoped:suggestion + +# Top-level statements: DO NOT USE +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0210-ide0211 +# This is enforced via a style preference set to error severity, not a language-level prohibition. +# Always use explicit class declarations with a proper namespace and Main method where applicable. +[*.cs] +csharp_style_prefer_top_level_statements = false:error + +[*.xml] +indent_style = space +indent_size = 2 + +# IDE0078: Use pattern matching +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0078 +[*.{cs,vb}] +dotnet_diagnostic.IDE0078.severity = none + +# IDE0290: Use primary constructor +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0290 +[*.{cs,vb}] +dotnet_diagnostic.IDE0290.severity = none + +# CA1200: Avoid using cref tags with a prefix +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1200 +[*.{cs,vb}] +dotnet_diagnostic.CA1200.severity = none + +# IDE0305: Use collection expression for fluent +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0305 +[*.{cs,vb}] +dotnet_diagnostic.IDE0305.severity = none + +# IDE0011: Add braces +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0011 +[*.{cs,vb}] +dotnet_diagnostic.IDE0011.severity = none + +# IDE0028: Use collection initializers or expressions +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0028 +[*.{cs,vb}] +dotnet_diagnostic.IDE0028.severity = none + +# IDE0039: Use collection expression for array +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0300 +[*.{cs,vb}] +dotnet_diagnostic.IDE0300.severity = none + +# IDE0031: Use collection expression for empty +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0301 +[*.{cs,vb}] +dotnet_diagnostic.IDE0301.severity = none + +# IDE0046: Use conditional expression for return +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0046 +[*.{cs,vb}] +dotnet_diagnostic.IDE0046.severity = none + +# IDE0047: Parentheses preferences +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0047-ide0048 +[*.{cs,vb}] +dotnet_diagnostic.IDE0047.severity = none + +# CA1716: Identifiers should not match keywords +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1716 +[*.{cs,vb}] +dotnet_diagnostic.CA1716.severity = none + +# CA1720: Identifiers should not contain type names +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1720 +[*.{cs,vb}] +dotnet_diagnostic.CA1720.severity = none + +# CA1846: Prefer AsSpan over Substring +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1846 +# Excluded while TFMs include netstandard2.0 +[*.{cs,vb}] +dotnet_diagnostic.CA1846.severity = none + +# CA1847: Use String.Contains(char) instead of String.Contains(string) with single characters +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1847 +# Excluded while TFMs include netstandard2.0 +[*.{cs,vb}] +dotnet_diagnostic.CA1847.severity = none + +# CA1865-CA1867: Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1865-ca1867 +# Excluded while TFMs include netstandard2.0 +[*.{cs,vb}] +dotnet_diagnostic.CA1865.severity = none +dotnet_diagnostic.CA1866.severity = none +dotnet_diagnostic.CA1867.severity = none + +# CA2263: Prefer generic overload when type is known +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2263 +# Excluded while TFMs include netstandard2.0 +[*.{cs,vb}] +dotnet_diagnostic.CA2263.severity = none + +# CA2249: Consider using String.Contains instead of String.IndexOf +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2249 +# Excluded while TFMs include netstandard2.0 +[*.{cs,vb}] +dotnet_diagnostic.CA2249.severity = none + +# IDE0022: Use expression body for methods +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0022 +[*.{cs,vb}] +dotnet_diagnostic.IDE0022.severity = none + +# IDE0032: Use auto-property +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0032 +[*.{cs,vb}] +dotnet_diagnostic.IDE0032.severity = none + +# Order modifiers +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0036 +# Excluded becuase of inconsistency with other analyzers +[*.{cs,vb}] +dotnet_diagnostic.IDE0036.severity = none + +# Order modifiers +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0036 +# Excluded becuase of inconsistency with other analyzers +[*.{cs,vb}] +dotnet_diagnostic.IDE0036.severity = none + +# Use 'System.Threading.Lock' +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0330 +# Excluded while TFMs are less than net9.0 +[*.{cs,vb}] +dotnet_diagnostic.IDE0330.severity = none From e26c35beda141434285cab745cf21b1fc80b2842 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Wed, 18 Feb 2026 17:57:37 +0100 Subject: [PATCH 03/10] =?UTF-8?q?=F0=9F=A4=96=20add=20agents.md=20for=20AI?= =?UTF-8?q?=20agents=20guidelines=20and=20project=20overview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 265 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5c50edc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,265 @@ +# AGENTS.md - Codebelt.Extensions.Xunit + +Guide for AI agents working on this .NET xUnit extensions library. + +## Project Overview + +A .NET library providing extensions for xUnit v3 testing framework. Supports multi-targeting: `net10.0`, `net9.0`, `netstandard2.0` (source) and `net48` (tests on Windows). + +## Repository Layout + +- Solution: `Codebelt.Extensions.Xunit.slnx` in repo root. +- `src/` — NuGet packages (shipped to nuget.org). +- `test/` — xUnit v3 unit and functional tests. +- `tuning/` — BenchmarkDotNet benchmarks. +- `tooling/` — internal CLI tools. +- `.nuget//` — per-package `README.md` and `PackageReleaseNotes.txt`. + +## Toolchain + +- .NET SDK with `LangVersion=latest`. +- Source TFMs: `net10.0;net9.0;netstandard2.0`. +- Test TFMs: `net10.0;net9.0` on Linux; adds `net48` on Windows. +- Benchmark TFMs: `net10.0;net9.0;netstandard2.0`. +- Central package management via `Directory.Packages.props` (`ManagePackageVersionsCentrally=true`). +- CI runs on Linux (ubuntu-24.04) and Windows (windows-2025), both X64 and ARM64. +- TFM compatibility is mandatory: proposals and code changes must work for all source TFMs. Do not assume `net9.0`/`net10.0` APIs exist in `netstandard2.0`; use conditional compilation (`#if NET9_0_OR_GREATER`) or compatible fallbacks where needed. + +## Build Commands + +```bash +# Build entire solution +dotnet build Codebelt.Extensions.Xunit.slnx + +# Build Release configuration +dotnet build Codebelt.Extensions.Xunit.slnx -c Release + +# Build with skipped assembly signing (for CI/external contributors) +dotnet build -p:SkipSignAssembly=true + +# Restore packages +dotnet restore Codebelt.Extensions.Xunit.slnx + +# Pack NuGet packages +dotnet pack -c Release +``` + +## Lint / Analyzers + +- No separate lint step; code style is enforced during build (`EnforceCodeStyleInBuild=true` for source projects). +- Analyzers are **disabled** for test and benchmark projects (`RunAnalyzers=false`, `AnalysisLevel=none`). +- Run `dotnet build -c Release` on source projects to surface style violations. + +## Test Commands + +```bash +# Run all tests +dotnet test + +# Run tests for specific project +dotnet test test/Codebelt.Extensions.Xunit.Tests + +# Run single test by fully qualified name +dotnet test --filter "FullyQualifiedName~TestTest" + +# Run tests with specific trait +dotnet test --filter "Category=Unit" + +# Run tests with coverage +dotnet test --collect:"XPlat Code Coverage" +``` + +### Benchmarks + +- Live under `tuning/`; run with `tooling/benchmark-runner`. +- Not unit tests; do not include in test runs. + +## Cursor / Copilot Rules + +- No Cursor rules (`.cursor/rules/` and `.cursorrules` are absent). +- Copilot rules live in `.github/copilot-instructions.md` — **must follow**. + +## Code Style and Conventions + +### General Principles +- Follow Framework Design Guidelines and Microsoft Engineering Guidelines. +- Adhere to SOLID, DRY, separation of concerns. +- Apply the boy scout rule; do not duplicate code. + +### Formatting +- 4 spaces for `.cs` files; 2 spaces for `.xml` (`.editorconfig`). +- Keep existing style in files; many modern analyzers are explicitly disabled. + +### Namespace Style +- **Prefer file-scoped namespaces** (`namespace Codebelt.Extensions.Xunit;`) for new files. +- The current majority of the codebase uses **block-scoped namespaces** — do not convert existing files unless explicitly asked. +- When editing an existing file, follow whichever style that file already uses. +- **Never use top-level statements.** Always use explicit class declarations with a proper namespace. + +### Disabled Analyzers (key rules — do NOT introduce these patterns) + +| Rule | What it forces | Why disabled | +|------|---------------|--------------| +| IDE0066 | switch expressions | style consistency | +| IDE0063 | using declarations | style consistency | +| IDE0290 | primary constructors | style consistency | +| IDE0022 | expression-bodied methods | style consistency | +| IDE0300/0301/0028/0305 | collection expressions | netstandard2.0 compat | +| CA1846/1847/1865-1867 | Span/char overloads | netstandard2.0 compat | +| IDE0330 | `System.Threading.Lock` | requires net9.0+ | +| Performance category | various | netstandard2.0 compat | + +### Namespaces +- **CRITICAL**: Test namespaces MUST match the System Under Test (SUT) exactly +- Do NOT append `.Tests` or `.Benchmarks` to namespaces +- Example: SUT `Codebelt.Extensions.Xunit` → Tests `Codebelt.Extensions.Xunit` (not `Codebelt.Extensions.Xunit.Tests`) +- Override `RootNamespace` in `.csproj` to match SUT namespace + +### Test Classes +- Always inherit from `Test` base class from `Codebelt.Extensions.Xunit` +- Constructor must accept `ITestOutputHelper output` and pass to base +- Class names end with `Test` (e.g., `DateSpanTest`) + +```csharp +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Codebelt.Extensions.Xunit // Same as SUT +{ + public class YourTestClass : Test + { + public YourTestClass(ITestOutputHelper output) : base(output) { } + } +} +``` + +### Imports +- Use `using Xunit;` - NOT `Xunit.Abstractions` (xUnit v3 removed this namespace) +- Use `using Xunit.v3;` when needed for xUnit v3 specific types +- Place System.* usings first, then third-party, then project +- Keep `using` directives explicit and minimal. +- Follow existing ordering; do not auto-reorder. + +### Naming Conventions +- Test methods: Use descriptive names with `Should` prefix + - Pattern: `Should{ExpectedResult}_When{Condition}` + - Example: `ShouldReturnTrue_WhenConditionIsMet` +- Use `[Fact]` for standard tests, `[Theory]` for parameterized tests +- Benchmark classes: End with `Benchmark` + +### Types and `var` +- Do not blindly enforce `var`; use explicit types when it improves clarity. +- IDE0008 (use explicit type) is disabled — either form is acceptable. + +### Error Handling +- Use guard clauses with `ArgumentNullException`, `ArgumentOutOfRangeException` +- **Always use `Validator` pattern** (e.g., `Validator.ThrowIfNull(param)`) +- Pattern: `if (param == null) { throw new ArgumentNullException(nameof(param)); }` only when Validator is not available +- Use `ArgumentOutOfRangeException` for validation with actual/expected values +- Prefer deterministic, testable error paths; never swallow exceptions. + +### XML Documentation +- Document all public/protected APIs with XML comments +- Use `` for type references +- Include `` tags for thrown exceptions +- Follow existing documentation style (see Hash.cs example) + +## Testing Guidelines + +### Test Doubles +- Preferred: dummies, fakes, stubs, spies +- Mocks allowed: Moq library for special circumstances only +- Never mock `IMarshaller`; use `JsonMarshaller` instance instead + +### Internal Members +- Do NOT use `InternalsVisibleTo` +- Test internal logic via public APIs that consume them +- Use Public Facade Testing pattern + +### Async Tests +- Override `InitializeAsync()` for async setup +- Use `ValueTask` for async operations (not `Task`) +- Implement `IAsyncLifetime` when needed via `Test` base class + +## Benchmark Guidelines + +- Place in `tuning/` folder or `*.Benchmarks` projects +- Use `[MemoryDiagnoser]` and `[GroupBenchmarksBy]` attributes +- Use `[Params]` for input variations +- Use `[GlobalSetup]` for initialization +- Namespaces follow same rule as tests (no `.Benchmarks` suffix) +- Use `[MemoryDiagnoser]`, `[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]`. +- Use `[GlobalSetup]` for expensive prep; keep measured methods focused. +- Use `[Params]` for multiple input sizes; use deterministic data; avoid external systems. +- Mark one method `Baseline = true`; use descriptive `Description` values. + +## Package Management + +- Uses Central Package Management (`Directory.Packages.props`) +- Do not add version numbers in individual `.csproj` files +- Test frameworks are centrally managed + +## CI/Build Notes + +- Assembly signing uses `xunit.snk` (skip for external builds) +- MinVer handles versioning based on Git tags +- Code coverage via coverlet +- SonarCloud and CodeQL analysis enabled + +## Release Notes + +- Per-package notes in `.nuget//PackageReleaseNotes.txt`. +- Keep updated for public API changes. + +## Commit Style (Gitmoji) + +This repo uses **gitmoji** commit messages — do **not** use Conventional Commits (`feat:`, `fix:`, etc.). + +Format: ` ` + +**Always use the actual Unicode emoji character**, not the GitHub shortcode (e.g., use `✨` not `:sparkles:`). + +Example: `✨ Add Test.Match wildcard overload` + +### Common Gitmojis + +| Emoji | Use for | +|-------|---------| +| ✨ | New feature | +| 🐛 | Bug fix | +| ♻️ | Refactoring | +| ✅ | Adding / updating unit test / functional test | +| 📝 | Documentation | +| ⚡ | Performance improvement | +| 🎨 | Code style / formatting | +| 🔥 | Removing code or files | +| 🚧 | Work in progress | +| 📦 | Package / dependency update | +| 🔧 | Configuration / tooling | +| 🚚 | Moving / renaming files | +| 💥 | Breaking change | +| 🩹 | Non-critical fix | + +### Rules + +1. **One emoji per commit** — each commit has exactly one primary gitmoji. +2. **Be specific** — choose the most appropriate emoji, not a generic one. +3. **Consistent scope** — use consistent scope names across commits. +4. **Clear messages** — the subject line should be understandable without a body. +5. **Atomic commits** — each commit should be independently buildable and testable. + +## Agent Workflow + +1. Identify the correct project area (`src/`, `test/`, `tuning/`, `tooling/`). +2. Follow namespace and naming rules **before** writing any code. +3. Before potentially refactoring any code, verify the code in question is well tested; if coverage is missing, add or update tests first to reduce regression risk. +4. Build the affected source project to check for style violations. +5. Run targeted tests when changing logic. +6. Keep changes minimal and consistent with existing local style. + +## Copilot Instructions + +See `.github/copilot-instructions.md` for detailed guidelines on: +- Writing unit tests +- Writing performance tests (BenchmarkDotNet) +- Writing XML documentation From 8038227ec2a499f5ea5500ee36dd10d87a89ceae Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Wed, 18 Feb 2026 20:12:56 +0100 Subject: [PATCH 04/10] =?UTF-8?q?=F0=9F=91=B7=20add=20service=20update=20a?= =?UTF-8?q?utomation=20workflows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/dispatch-targets.json | 7 + .github/scripts/bump-nuget.py | 109 ++++++++++++++++ .github/workflows/service-update.yml | 158 +++++++++++++++++++++++ .github/workflows/trigger-downstream.yml | 78 +++++++++++ 4 files changed, 352 insertions(+) create mode 100644 .github/dispatch-targets.json create mode 100644 .github/scripts/bump-nuget.py create mode 100644 .github/workflows/service-update.yml create mode 100644 .github/workflows/trigger-downstream.yml diff --git a/.github/dispatch-targets.json b/.github/dispatch-targets.json new file mode 100644 index 0000000..f510e0c --- /dev/null +++ b/.github/dispatch-targets.json @@ -0,0 +1,7 @@ +[ + "benchmarkdotnet", + "newtonsoft-json", + "aws-signature-v4", + "unitify", + "yamldotnet" +] diff --git a/.github/scripts/bump-nuget.py b/.github/scripts/bump-nuget.py new file mode 100644 index 0000000..e077af3 --- /dev/null +++ b/.github/scripts/bump-nuget.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +""" +Bumps PackageVersion entries in Directory.Packages.props. +- For packages published by the triggering source repo: uses the known TRIGGER_VERSION. +- For all other packages: queries NuGet API for latest stable version. +- Preserves all XML formatting via regex (no ElementTree rewriting). +""" + +import re, os, urllib.request, json, sys + +TRIGGER_SOURCE = os.environ.get("TRIGGER_SOURCE", "") +TRIGGER_VERSION = os.environ.get("TRIGGER_VERSION", "") + +# Maps source repo name → NuGet package ID prefixes published by that repo. +# Keep this aligned with what each repo actually publishes. +SOURCE_PACKAGE_MAP: dict[str, list[str]] = { + "cuemon": [ + "Cuemon.", + ], + "xunit": [ + "Codebelt.Extensions.Xunit", + ], + "benchmarkdotnet": [ + "Codebelt.Extensions.BenchmarkDotNet", + ], + "bootstrapper": [ + "Codebelt.Bootstrapper", + ], + "newtonsoft-json": [ + "Codebelt.Extensions.Newtonsoft.Json", + "Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft", + ], + "aws-signature-v4": [ + "Codebelt.Extensions.AspNetCore.Authentication.AwsSignature", + ], + "unitify": [ + "Codebelt.Unitify", + ], + "yamldotnet": [ + "Codebelt.Extensions.YamlDotNet", + "Codebelt.Extensions.AspNetCore.Mvc.Formatters.Text.Yaml", + ], + "globalization": [ + "Codebelt.Extensions.Globalization", + ], + "asp-versioning": [ + "Codebelt.Extensions.Asp.Versioning", + ], + "swashbuckle-aspnetcore": [ + "Codebelt.Extensions.Swashbuckle", + ], + "savvyio": [ + "Savvyio.", + ], + "shared-kernel": [], +} + + +def nuget_latest(pkg: str) -> str | None: + """Query NuGet flat container API for the latest stable version.""" + try: + url = f"https://api.nuget.org/v3-flatcontainer/{pkg.lower()}/index.json" + with urllib.request.urlopen(url, timeout=15) as r: + versions = json.loads(r.read())["versions"] + stable = [v for v in versions if not re.search(r"-", v)] + return stable[-1] if stable else None + except Exception as e: + print(f" SKIP {pkg}: {e}", file=sys.stderr) + return None + + +def triggered_version(pkg: str) -> str | None: + """Return TRIGGER_VERSION if pkg is published by the triggering source repo.""" + if not TRIGGER_VERSION or not TRIGGER_SOURCE: + return None + prefixes = SOURCE_PACKAGE_MAP.get(TRIGGER_SOURCE, []) + if any(pkg.startswith(p) for p in prefixes): + return TRIGGER_VERSION + return None + + +with open("Directory.Packages.props", "r") as f: + content = f.read() + +changes = [] + + +def replace_version(m: re.Match) -> str: + pkg = m.group(1) + current = m.group(2) + + new_ver = triggered_version(pkg) or nuget_latest(pkg) or current + + if new_ver != current: + changes.append(f" {pkg}: {current} → {new_ver}") + return m.group(0).replace(f'Version="{current}"', f'Version="{new_ver}"') + + +pattern = re.compile(r'> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Determine new version for this repo + id: newver + run: | + CURRENT=$(grep -oP '(?<=## \[)[\d.]+(?=\])' CHANGELOG.md | head -1) + NEW=$(echo "$CURRENT" | awk -F. '{printf "%s.%s.%d", $1, $2, $3+1}') + BRANCH="v${NEW}/service-update" + echo "current=$CURRENT" >> $GITHUB_OUTPUT + echo "new=$NEW" >> $GITHUB_OUTPUT + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + + - name: Generate codebelt-aicia token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.CODEBELT_AICIA_APP_ID }} + private-key: ${{ secrets.CODEBELT_AICIA_PRIVATE_KEY }} + owner: codebeltnet + + - name: Bump NuGet packages + run: python3 .github/scripts/bump-nuget.py + env: + TRIGGER_SOURCE: ${{ steps.trigger.outputs.source }} + TRIGGER_VERSION: ${{ steps.trigger.outputs.version }} + + - name: Update PackageReleaseNotes.txt + run: | + NEW="${{ steps.newver.outputs.new }}" + for f in .nuget/*/PackageReleaseNotes.txt; do + [ -f "$f" ] || continue + TFM=$(grep -m1 "^Availability:" "$f" | sed 's/Availability: //' || echo ".NET 10, .NET 9 and .NET Standard 2.0") + ENTRY="Version ${NEW}\nAvailability: ${TFM}\n \n# ALM\n- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)\n \n" + { printf "$ENTRY"; cat "$f"; } > "$f.tmp" && mv "$f.tmp" "$f" + done + + - name: Update CHANGELOG.md + run: | + python3 - <<'EOF' + import os, re + from datetime import date + new_ver = os.environ['NEW_VERSION'] + today = date.today().isoformat() + entry = f"## [{new_ver}] - {today}\n\nThis is a service update that focuses on package dependencies.\n\n" + with open("CHANGELOG.md") as f: + content = f.read() + idx = content.find("## [") + content = (content[:idx] + entry + content[idx:]) if idx != -1 else (content + entry) + with open("CHANGELOG.md", "w") as f: + f.write(content) + print(f"CHANGELOG updated for v{new_ver}") + EOF + env: + NEW_VERSION: ${{ steps.newver.outputs.new }} + + - name: Bump test runner Docker tag + run: | + [ -f testenvironments.json ] || exit 0 + LATEST=$(curl -sf "https://hub.docker.com/v2/repositories/codebeltnet/ubuntu-testrunner/tags/?page_size=10&ordering=last_updated" \ + | python3 -c " + import sys, json + tags = [t['name'] for t in json.load(sys.stdin)['results'] if 'latest' not in t['name']] + print(tags[0] if tags else '') + ") + [ -n "$LATEST" ] && \ + sed -i "s|codebeltnet/ubuntu-testrunner:[^\"]*|codebeltnet/ubuntu-testrunner:${LATEST}|g" \ + testenvironments.json && echo "Bumped to $LATEST" + + - name: Bump NGINX Alpine version + run: | + [ -f .docfx/Dockerfile.docfx ] || exit 0 + LATEST=$(curl -sf "https://hub.docker.com/v2/repositories/library/nginx/tags/?page_size=50&name=alpine&ordering=last_updated" \ + | python3 -c " + import sys, json, re + data = json.load(sys.stdin) + tags = [t['name'] for t in data['results'] if re.match(r'^\d+\.\d+\.\d+-alpine$', t['name'])] + if tags: + print(sorted(tags, key=lambda v: list(map(int, v.replace('-alpine','').split('.'))))[-1]) + ") + [ -n "$LATEST" ] && \ + sed -i "s|NGINX_VERSION=.*|NGINX_VERSION=${LATEST}|g" \ + .docfx/Dockerfile.docfx && echo "Bumped NGINX to $LATEST" + + - name: Show diff (dry run) + if: ${{ github.event.inputs.dry_run == 'true' }} + run: git diff + + - name: Create branch and open PR + if: ${{ github.event.inputs.dry_run != 'true' }} + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + NEW="${{ steps.newver.outputs.new }}" + BRANCH="${{ steps.newver.outputs.branch }}" + SOURCE="${{ steps.trigger.outputs.source }}" + SRC_VER="${{ steps.trigger.outputs.version }}" + + git config user.name "codebelt-aicia[bot]" + git config user.email "codebelt-aicia[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add -A + git diff --cached --quiet && echo "Nothing changed - skipping PR." && exit 0 + git commit -m "V${NEW}/service update" + git push origin "$BRANCH" + + echo "This is a service update that focuses on package dependencies." > pr_body.txt + echo "" >> pr_body.txt + echo "Automated changes:" >> pr_body.txt + echo "- NuGet package versions bumped to latest compatible" >> pr_body.txt + echo "- PackageReleaseNotes.txt updated for v${NEW}" >> pr_body.txt + echo "- CHANGELOG.md entry added for v${NEW}" >> pr_body.txt + echo "- Docker test runner tag bumped (if applicable)" >> pr_body.txt + echo "- NGINX Alpine version bumped (if applicable)" >> pr_body.txt + echo "" >> pr_body.txt + echo "Generated by codebelt-aicia" >> pr_body.txt + echo "Triggered by: ${SOURCE} @ ${SRC_VER}" >> pr_body.txt + + gh pr create --title "V${NEW}/service update" --body-file pr_body.txt --base main --head "$BRANCH" --assignee gimlichael diff --git a/.github/workflows/trigger-downstream.yml b/.github/workflows/trigger-downstream.yml new file mode 100644 index 0000000..29eb29c --- /dev/null +++ b/.github/workflows/trigger-downstream.yml @@ -0,0 +1,78 @@ +name: Trigger Downstream Service Updates + +on: + release: + types: [published] + +jobs: + dispatch: + if: github.event.release.prerelease == false + runs-on: ubuntu-24.04 + permissions: + contents: read + + steps: + - name: Checkout (to read dispatch-targets.json) + uses: actions/checkout@v4 + + - name: Check for dispatch targets + id: check + run: | + if [ ! -f .github/dispatch-targets.json ]; then + echo "No dispatch-targets.json found, skipping." + echo "has_targets=false" >> $GITHUB_OUTPUT + exit 0 + fi + COUNT=$(python3 -c "import json; print(len(json.load(open('.github/dispatch-targets.json'))))") + echo "has_targets=$([ $COUNT -gt 0 ] && echo true || echo false)" >> $GITHUB_OUTPUT + + - name: Extract version from release tag + if: steps.check.outputs.has_targets == 'true' + id: version + run: | + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Generate codebelt-aicia token + if: steps.check.outputs.has_targets == 'true' + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.CODEBELT_AICIA_APP_ID }} + private-key: ${{ secrets.CODEBELT_AICIA_PRIVATE_KEY }} + owner: codebeltnet + + - name: Dispatch to downstream repos + if: steps.check.outputs.has_targets == 'true' + run: | + python3 - <<'EOF' + import json, urllib.request, os, sys + + targets = json.load(open('.github/dispatch-targets.json')) + token = os.environ['GH_TOKEN'] + version = os.environ['VERSION'] + source = os.environ['SOURCE_REPO'] + + for repo in targets: + url = f'https://api.github.com/repos/codebeltnet/{repo}/dispatches' + payload = json.dumps({ + 'event_type': 'codebelt-service-update', + 'client_payload': { + 'source_repo': source, + 'source_version': version + } + }).encode() + req = urllib.request.Request(url, data=payload, method='POST', headers={ + 'Authorization': f'Bearer {token}', + 'Accept': 'application/vnd.github+json', + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28' + }) + with urllib.request.urlopen(req) as r: + print(f'✓ Dispatched to {repo}: HTTP {r.status}') + EOF + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + VERSION: ${{ steps.version.outputs.version }} + SOURCE_REPO: ${{ github.event.repository.name }} From 38760390bf155816cfdc8ff6e7e2859d8c81115f Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Wed, 18 Feb 2026 21:07:50 +0100 Subject: [PATCH 05/10] Update .editorconfig Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .editorconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index df96769..3475548 100644 --- a/.editorconfig +++ b/.editorconfig @@ -176,13 +176,13 @@ dotnet_diagnostic.IDE0032.severity = none # Order modifiers # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0036 -# Excluded becuase of inconsistency with other analyzers +# Excluded because of inconsistency with other analyzers [*.{cs,vb}] dotnet_diagnostic.IDE0036.severity = none # Order modifiers # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0036 -# Excluded becuase of inconsistency with other analyzers +# Excluded because of inconsistency with other analyzers [*.{cs,vb}] dotnet_diagnostic.IDE0036.severity = none From b9082d61f41bc82e101e1c8d82d61b951db1c2d5 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Wed, 18 Feb 2026 21:08:41 +0100 Subject: [PATCH 06/10] Update AGENTS.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- AGENTS.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5c50edc..5083095 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -184,11 +184,8 @@ namespace Codebelt.Extensions.Xunit // Same as SUT ## Benchmark Guidelines - Place in `tuning/` folder or `*.Benchmarks` projects -- Use `[MemoryDiagnoser]` and `[GroupBenchmarksBy]` attributes -- Use `[Params]` for input variations -- Use `[GlobalSetup]` for initialization - Namespaces follow same rule as tests (no `.Benchmarks` suffix) -- Use `[MemoryDiagnoser]`, `[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]`. +- Use `[MemoryDiagnoser]` and `[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]` attributes. - Use `[GlobalSetup]` for expensive prep; keep measured methods focused. - Use `[Params]` for multiple input sizes; use deterministic data; avoid external systems. - Mark one method `Baseline = true`; use descriptive `Description` values. From 49b742a7ca900af07ae6b43808c7c5773f22aed1 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Wed, 18 Feb 2026 21:09:40 +0100 Subject: [PATCH 07/10] Update .github/scripts/bump-nuget.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/scripts/bump-nuget.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/scripts/bump-nuget.py b/.github/scripts/bump-nuget.py index e077af3..7117b61 100644 --- a/.github/scripts/bump-nuget.py +++ b/.github/scripts/bump-nuget.py @@ -96,7 +96,13 @@ def replace_version(m: re.Match) -> str: return m.group(0).replace(f'Version="{current}"', f'Version="{new_ver}"') -pattern = re.compile(r']*\bInclude="([^"]+)")' + r'(?=[^>]*\bVersion="([^"]+)")' + r'[^>]*>', + re.DOTALL, +) new_content = pattern.sub(replace_version, content) if changes: From b9cd90de6a20fc889a4e299bf49a79f20ce880b3 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Wed, 18 Feb 2026 21:11:10 +0100 Subject: [PATCH 08/10] Update .github/scripts/bump-nuget.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/scripts/bump-nuget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/scripts/bump-nuget.py b/.github/scripts/bump-nuget.py index 7117b61..375f41a 100644 --- a/.github/scripts/bump-nuget.py +++ b/.github/scripts/bump-nuget.py @@ -7,13 +7,14 @@ """ import re, os, urllib.request, json, sys +from typing import Dict, List TRIGGER_SOURCE = os.environ.get("TRIGGER_SOURCE", "") TRIGGER_VERSION = os.environ.get("TRIGGER_VERSION", "") # Maps source repo name → NuGet package ID prefixes published by that repo. # Keep this aligned with what each repo actually publishes. -SOURCE_PACKAGE_MAP: dict[str, list[str]] = { +SOURCE_PACKAGE_MAP: Dict[str, List[str]] = { "cuemon": [ "Cuemon.", ], From ffcf90ce8d0ed7cfe094e790e7fc8491610f63a7 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Wed, 18 Feb 2026 21:12:27 +0100 Subject: [PATCH 09/10] Update .editorconfig Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .editorconfig | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.editorconfig b/.editorconfig index 3475548..85dcc19 100644 --- a/.editorconfig +++ b/.editorconfig @@ -180,12 +180,6 @@ dotnet_diagnostic.IDE0032.severity = none [*.{cs,vb}] dotnet_diagnostic.IDE0036.severity = none -# Order modifiers -# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0036 -# Excluded because of inconsistency with other analyzers -[*.{cs,vb}] -dotnet_diagnostic.IDE0036.severity = none - # Use 'System.Threading.Lock' # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0330 # Excluded while TFMs are less than net9.0 From 9fbd7fc90d269c145efed19c85818db08dd0a5bf Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Wed, 18 Feb 2026 21:14:01 +0100 Subject: [PATCH 10/10] Update .github/workflows/service-update.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/service-update.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/service-update.yml b/.github/workflows/service-update.yml index ee3e586..e2e5202 100644 --- a/.github/workflows/service-update.yml +++ b/.github/workflows/service-update.yml @@ -153,6 +153,10 @@ jobs: echo "- NGINX Alpine version bumped (if applicable)" >> pr_body.txt echo "" >> pr_body.txt echo "Generated by codebelt-aicia" >> pr_body.txt - echo "Triggered by: ${SOURCE} @ ${SRC_VER}" >> pr_body.txt + if [ -n "$SOURCE" ] && [ -n "$SRC_VER" ]; then + echo "Triggered by: ${SOURCE} @ ${SRC_VER}" >> pr_body.txt + else + echo "Triggered by: manual workflow dispatch" >> pr_body.txt + fi gh pr create --title "V${NEW}/service update" --body-file pr_body.txt --base main --head "$BRANCH" --assignee gimlichael