diff --git a/.editorconfig b/.editorconfig index de52618..2538f17 100644 --- a/.editorconfig +++ b/.editorconfig @@ -47,52 +47,21 @@ end_of_line = lf [*.{cmd,bat,ps1}] end_of_line = crlf +# --- .NET-only below: C# and ReSharper style. Everything above is the line-ending +# governance every derived repo carries; a non-.NET repo may drop from here down. --- + # C# files [*.cs] # Default to suggestion severity dotnet_analyzer_diagnostic.severity = suggestion -# AnalysisMode=All (Directory.Build.props) enables every analyzer rule as a -# build warning, overriding the bulk suggestion default above on a per-rule -# basis; combined with TreatWarningsAsErrors that breaks the build on existing -# brownfield code. Relax the specific rules below back to suggestion — each is -# a deliberate, documented exception rather than a latent defect: -# CA1002 Public APIs intentionally expose List (e.g. FileEx.EnumerateDirectory, -# StringHistory.StringList); changing to Collection would break the -# published InsaneGenius.Utilities surface. -# CA1024 Download.GetUri is intentionally a method, not a property. -# CA1034 Unavoidable nested types generated by C# 14 `extension` members in -# Extensions.cs (the file already suppresses the related CA1708). -# CA1054 Download URL parameters are intentionally `string`, not `System.Uri`. -# CA1063 Existing IDisposable implementations are intentionally simple. -# CA1307 String operations rely on the default comparison; existing behavior -# is intentional and unchanged. -# CA1515 Library/console/test public types are intentionally public. -# CA1823 xUnit fixture fields are injected for lifetime/collection wiring and -# are not always referenced directly. -# CA1849 A few synchronous calls inside async paths are kept intentionally. -# CA2000 Stream/disposable ownership is frequently transferred (returned or -# stored), so scope-based disposal analysis reports false positives. -# CA2007 Library `await using` / `await foreach` disposal sites; the awaited -# async calls already use ConfigureAwait(false), and rewriting the -# using-declarations into ConfigureAwait blocks hurts readability. -# CA5394 Random is used for retry jitter / temp-name generation, not security. -# (IL3058 — Serilog not AOT-annotated — is a compiler/linker-level warning with +# Per-rule analyzer exceptions live in the owning project's `.editorconfig` +# (Utilities/, UtilitiesTests/) at the narrowest scope that fits, each with its +# own justification - see CODESTYLE.md "Analyzer Diagnostics and Suppressions". +# (IL3058 - Serilog not AOT-annotated - is a compiler/linker-level warning with # no source location, so it can't be set here; it's handled via NoWarn in the # AOT project files instead.) -dotnet_diagnostic.CA1002.severity = suggestion -dotnet_diagnostic.CA1024.severity = suggestion -dotnet_diagnostic.CA1034.severity = suggestion -dotnet_diagnostic.CA1054.severity = suggestion -dotnet_diagnostic.CA1063.severity = suggestion -dotnet_diagnostic.CA1307.severity = suggestion -dotnet_diagnostic.CA1515.severity = suggestion -dotnet_diagnostic.CA1823.severity = suggestion -dotnet_diagnostic.CA1849.severity = suggestion -dotnet_diagnostic.CA2000.severity = suggestion -dotnet_diagnostic.CA2007.severity = suggestion -dotnet_diagnostic.CA5394.severity = suggestion csharp_indent_block_contents = true csharp_indent_braces = false diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5f39aaf..da1fc8a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,423 +1,36 @@ -# GitHub Copilot Instructions for Utilities Project +# Copilot Instructions -## Project Overview +Repository conventions for GitHub Copilot (and any other AI agent reading this file). -This is a .NET utility library that provides generally useful C# classes and extensions. The project targets .NET 10 and includes AOT (Ahead-of-Time) compilation support for optimized runtime performance. +The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract this file's runbook implements. This file is intentionally narrow: commit/PR-title conventions (summarized inline so VS Code's commit-message and PR-title generators have them) plus the GitHub Copilot Review Runbook. -## Code Style and Standards +For code-style rules, see [`CODESTYLE.md`](../CODESTYLE.md) at the repo root - one guide with a General section plus a .NET language section. -### General Guidelines +Do not duplicate language-specific rules here. **Project-specific conventions and API/behavioral contracts also belong in [AGENTS.md](../AGENTS.md), not here** - this file is intentionally limited to the inline commit/PR-title summary and the GitHub Copilot Review Runbook. Non-Copilot agents (Claude Code, Codex, Cursor, ...) are not directed to this file and don't read it by default, so any rule a reviewer must honor has to live in `AGENTS.md` to be provider-independent. -- Follow C# coding conventions and .NET best practices -- Use meaningful variable and method names -- Keep methods focused and single-purpose -- **Add comprehensive XML documentation comments for ALL public APIs** (required) -- Maintain consistency with existing code style -- Follow the existing patterns in the codebase +## Commit Messages and Pull Request Titles -### Formatting Requirements +Summarized for VS Code's generators; the full rules, rationale, and examples are in [AGENTS.md "Pull Request Title and Commit Message Conventions"](../AGENTS.md#pull-request-title-and-commit-message-conventions). -**IMPORTANT:** This project uses **Husky.Net** pre-commit hooks that automatically enforce formatting: - -1. **CSharpier** is run first for code formatting -2. **dotnet format** is run second for style enforcement - -#### Formatting Workflow - -```bash -# Always format with CSharpier FIRST after editing code -dotnet csharpier . - -# Then run dotnet format to apply .editorconfig rules -dotnet format - -# Verify no changes needed -dotnet format --verify-no-changes -``` - -#### Key Formatting Rules (from .editorconfig) - -- **No `var` keyword**: Use explicit types everywhere - - ```csharp - // ✅ CORRECT - string text = "hello"; - List numbers = []; - - // ❌ WRONG - var text = "hello"; - var numbers = new List(); - ``` - -- **Indentation**: 4 spaces (not tabs) -- **Line endings**: CRLF (Windows) -- **Charset**: UTF-8 -- **Final newline**: Required -- **Trailing whitespace**: Not allowed -- **File-scoped namespaces**: Required -- **Collection expressions**: Preferred `[]` over `new List()` - -#### Pre-Commit Hook - -The Husky.Net pre-commit hook automatically runs: - -1. `dotnet csharpier .` - Code formatting -2. `dotnet format` - Style enforcement - -**Commits will be rejected if formatting fails!** - -### .NET 10 and AOT Considerations - -- The project targets .NET 10 with PublishAot enabled -- Avoid reflection where possible (not AOT-friendly) -- Use source generators instead of runtime reflection when applicable -- Be mindful of trim warnings and compatibility -- Ensure all code is AOT-compatible -- Test AOT compatibility with `dotnet publish` - -## Project Structure - -- **Utilities/**: Main library project containing utility classes - - `CommandLineEx.cs`: Command-line argument parsing utilities - - `ConsoleEx.cs`: Console interaction helpers with color support - - `Download.cs`: HTTP download utilities (sync and async) - - `Extensions.cs`: Extension methods (string compression, logger error handling) - - `FileEx.cs`: File and directory operation utilities with retry logic (sync and async) - - `FileExOptions.cs`: Configuration options for FileEx operations - - `Format.cs`: Byte size formatting utilities (binary and decimal) - - `LogOptions.cs`: Global logging configuration - - `StringCompression.cs`: String compression/decompression using Deflate (sync and async) - - `StringHistory.cs`: Bounded string history buffer - -- **Sandbox/**: Console application for testing and experimentation -- **UtilitiesTests/**: Unit tests using xUnit - -## Testing - -- Use xUnit for all tests -- Follow AAA pattern (Arrange, Act, Assert) -- Test file names should match the class being tested with "Tests" suffix -- Run tests frequently during development -- Maintain good test coverage for public APIs -- **Add tests for all new async methods** -- Consider edge cases and error conditions in tests - -## Dependencies - -- **Serilog**: Logging framework (required for LogOptions and error handling) -- **Microsoft.SourceLink.GitHub**: Source linking for debugging -- **xUnit**: Testing framework -- Keep dependencies minimal and well-justified -- Update package references to latest stable versions when appropriate - -## Common Tasks - -### Building - -Run the ".NET Build" task or use: `dotnet build` - -### Publishing - -Run the ".NET Publish" task or use: `dotnet publish` - -### Formatting (REQUIRED before commit) - -```bash -# Step 1: Format with CSharpier -dotnet csharpier . - -# Step 2: Apply dotnet format rules -dotnet format - -# Step 3: Verify (this is what pre-commit hook checks) -dotnet format --verify-no-changes -``` - -### Running Tests - -Use: `dotnet test` - -## Commit Guidelines - -### Pre-Commit Process (Automated by Husky.Net) - -The following happens automatically on every commit: - -1. ✅ CSharpier formats all C# files -2. ✅ dotnet format applies .editorconfig rules -3. ✅ Commit proceeds if formatting passes -4. ❌ Commit is rejected if formatting fails - -### Manual Pre-Commit Checklist - -Before committing, ensure: - -- [ ] Code formatted with CSharpier (`dotnet csharpier .`) -- [ ] Style rules applied (`dotnet format`) -- [ ] No formatting issues (`dotnet format --verify-no-changes`) -- [ ] All tests passing (`dotnet test`) -- [ ] Build successful (`dotnet build`) -- [ ] No `var` keywords used -- [ ] XML documentation complete -- [ ] Commit message is clear and descriptive - -### Commit Message Format - -Follow conventional commit format: - -```text -(): - -[optional body] - -[optional footer] -``` - -Examples: - -- `feat(download): add async download methods` -- `fix(fileex): correct boundary condition in DeleteDirectory` -- `docs(readme): update async method examples` -- `test(compression): add async compression tests` - -## Package Information - -- **Package ID**: InsaneGenius.Utilities -- **Namespace**: InsaneGenius.Utilities -- **License**: MIT -- **Repository**: -- **Target Framework**: .NET 10 -- **C# Version**: 14.0 -- **Version**: 3.5 (managed by Nerdbank.GitVersioning) - -## When Adding New Features - -1. **Consider AOT compatibility from the start** -2. **Add comprehensive XML documentation** (required for all public APIs) -3. **Create corresponding unit tests** (including async versions) -4. Update README.md if adding significant functionality -5. Ensure backward compatibility when modifying existing APIs -6. Consider performance implications -7. **Use async/await for I/O-bound operations** with proper cancellation token support -8. Handle exceptions appropriately with logging via LogOptions.Logger -9. **Follow existing patterns** (e.g., retry logic, bool return values, exception handling) -10. **Format with CSharpier before running dotnet format** - -## Code Generation Preferences - -### Modern C# Features (C# 14) - -- Prefer modern C# language features (pattern matching, records, file-scoped namespaces, etc.) -- Use nullable reference types consistently with `ArgumentNullException.ThrowIfNull()` -- Leverage expression-bodied members where appropriate -- Use collection expressions `[]` for initialization -- Use `extension` keyword for extension methods (inside static class) -- Prefer LINQ for data transformations -- Use primary constructors where appropriate - -### Async/Await Patterns - -- **Always use `ConfigureAwait(false)` in library code** -- Provide async versions of I/O-bound methods -- Use `CancellationToken` parameters (default to `default`) -- Use `await using` for async disposal -- Replace blocking calls (`.GetAwaiter().GetResult()`) with proper async -- Use `Task.Delay()` instead of `Thread.Sleep()` in async methods -- Use `Memory` and `Span` for async I/O operations - -### Input Validation - -- Use `ArgumentNullException.ThrowIfNull()` for null checks -- Validate parameters early in methods -- Document all exceptions in XML comments - -### Resource Management - -- Use `using` statements for proper disposal -- Use `await using` for async disposal -- Avoid explicit `.Close()` calls (using handles it) -- Use `leaveOpen` parameter when appropriate - -### Thread Safety - -- Use `Lazy` for thread-safe initialization -- Use `Lock` (C# 13+) instead of `object` for locks -- Avoid static mutable state -- Document thread-safety guarantees - -## Security Considerations - -- Validate all user inputs -- Use secure defaults -- Avoid hardcoding sensitive information -- Follow principle of least privilege -- Use secure random number generation when needed -- Be careful with file path manipulation - -## Performance Guidelines - -- Profile before optimizing -- Be mindful of allocations -- Use `Span` and `Memory` for performance-critical code -- Consider using object pooling for frequently allocated objects -- Use `ValueTask` for async methods that may complete synchronously -- Leverage AOT benefits for startup time and memory usage -- Avoid unnecessary string allocations -- Use `StringBuilder` for string concatenation in loops - -## Common Patterns in This Project - -### Error Handling - -```csharp -try -{ - // Operation -} -catch (IOException e) when (LogOptions.Logger.LogAndHandle(e)) -{ - // Retry or return false -} -catch (Exception e) when (LogOptions.Logger.LogAndHandle(e)) -{ - return false; -} -``` - -### Retry Logic - -- Use `Options.RetryCount` for retry attempts -- Use `Options.Cancel.IsCancellationRequested` for cancellation -- Use `Task.Delay()` for async waits -- Log retry attempts with `LogOptions.Logger.Information()` - -### Method Signatures - -- I/O methods return `bool` for success/failure -- Async methods have `Async` suffix -- Async methods include optional `CancellationToken cancellationToken = default` -- Use `out` parameters for additional return values - -### XML Documentation - -- Always include `` for all public members -- Document all `` with descriptions -- Document `` with descriptions -- Document all possible `` types -- Use `` for additional context -- Reference other types with `` - -## Anti-Patterns to Avoid - -❌ Sync-over-async: `.GetAwaiter().GetResult()`, `.Wait()`, `.Result` -❌ Missing `ConfigureAwait(false)` in library code -❌ Missing XML documentation on public APIs -❌ Not using `ArgumentNullException.ThrowIfNull()` -❌ Explicit `.Close()` calls when using `using` -❌ Missing cancellation token support in async methods -❌ Race conditions in static initialization -❌ Reflection (not AOT-compatible) -❌ Missing tests for async methods -❌ Using `var` keyword (explicit types required by .editorconfig) -❌ Forgetting to run CSharpier before dotnet format - -## File-Specific Notes - -### Download.cs - -- Uses thread-safe `Lazy` initialization -- Provides both sync and async versions -- Returns tuples from async methods for multiple values -- Uses `HttpCompletionOption.ResponseHeadersRead` for efficiency - -### FileEx.cs - -- All I/O methods have async versions -- Uses `Options` for retry configuration -- Returns `bool` for success/failure -- Supports cancellation via `Options.Cancel` and method parameter - -### StringCompression.cs - -- Supports configurable compression levels -- Has both sync and async versions -- Uses `leaveOpen` for stream management -- Proper error documentation - -### Extensions.cs - -- Uses C# 14 `extension` keyword -- Must be inside static class -- Provides extension methods for string compression and logger error handling - -## Development Workflow - -### Making Changes - -1. Edit code -2. **Run CSharpier**: `dotnet csharpier .` -3. **Run dotnet format**: `dotnet format` -4. Build: `dotnet build` -5. Test: `dotnet test` -6. Commit (Husky.Net pre-commit hook will verify formatting) - -### Before Committing - -```bash -# Format code (REQUIRED ORDER) -dotnet csharpier . -dotnet format - -# Verify -dotnet format --verify-no-changes -dotnet build -dotnet test - -# Commit -git add . -git commit -m "feat: your message" -# Husky.Net hook runs automatically -``` - -### If Pre-Commit Hook Fails - -```bash -# Hook will show formatting errors -# Re-run formatters -dotnet csharpier . -dotnet format - -# Try commit again -git commit -m "feat: your message" -``` - -## Tools Required - -- .NET 10 SDK -- CSharpier (installed as dotnet tool) -- Husky.Net (installed as dotnet tool) -- Visual Studio 2022 or VS Code with C# extension - -## EditorConfig Integration - -The project uses `.editorconfig` for style enforcement. Key rules: - -- `csharp_style_var_*` = **false** (no var keyword) -- `csharp_style_namespace_declarations` = **file_scoped** -- `csharp_prefer_system_threading_lock` = **true** -- `dotnet_style_prefer_collection_expression` = **when_types_loosely_match** - -Visual Studio and Rider automatically apply these settings. VS Code requires the EditorConfig extension. +- Imperative subject, <= 72 characters, no trailing period; optional blank-line-separated body for the non-obvious *why*. +- US English, title case with lowercase short bind words; no vague titles, no `Co-Authored-By:` unless asked, no release-bump magnitude (NBGV handles versioning). Dependabot's `Bump X from Y to Z` titles are fine. +- develop PRs squash-merge (`gh pr merge --squash`), main PRs merge-commit (`--merge`); a mismatched flag is rejected by branch protection. ## GitHub Copilot Review Runbook +> This runbook implements the [AGENTS.md "PR Review Etiquette"](../AGENTS.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). + Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it. ### Triggering and Polling -Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off). +Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This drives the loop end-to-end without a UI hand-off. + +**A review with no inline comments is still a completed review - not a failure, and not a reason to ask the maintainer to re-trigger.** Copilot very often posts a single formal review (GraphQL `state: COMMENTED`) whose body ends with "...reviewed N of N changed files ... and generated no comments" and adds **zero** inline threads. That review carries the head `commit.oid` and fully satisfies the loop - it is the clean-pass success case. Never read "no inline comments" as "the review didn't run," and never re-request or escalate to the maintainer because comments are absent. -> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them. +**Round 1 is normally auto-seeded - poll for it before trying to self-trigger.** Auto-review-on-open supplies the first review with no `botIds` call needed, but it can lag one to three minutes. After opening a PR (or the first push), **poll** for a Copilot review on the head SHA (see [Verify Review Covered Current Head](#verify-review-covered-current-head)) before concluding none ran. The `requestReviews` mutation below is for **re-requesting on later pushes** (a new head SHA); by then a prior review exists, so its bot node id is readable. A missing bot node id on round 1 therefore means "the auto-review has not landed yet - wait and poll," **not** "ask the maintainer to kick it off." + +> **The reviewer login differs by API.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them. ```sh # 1. PR node id + the Copilot reviewer's bot node id (read from any existing @@ -443,7 +56,7 @@ mutation($pr: ID!, $bot: ID!) { }' -F pr="$PR_NODE" -F bot="$BOT_ID" ``` -The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR - the auto-review-on-open normally supplies the first one. If no Copilot review exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request. +The bot node id is read from an existing Copilot **formal** review (`pullRequest.reviews`), so step 1 needs at least one prior formal review on the PR - the auto-review-on-open normally supplies the first one (it may have **no inline comments**; that still counts, and its bot node id is still readable). Poll for it (give auto-review-on-open a few minutes) before deciding it is missing. If Copilot posted **only an issue comment** and no formal review, the head is covered but `reviews` yields no bot node id - read the id from the Copilot issue comment's author by querying the PR's issue comments in GraphQL (`pullRequest.comments` -> author `... on Bot { id }`), or request `Copilot` once through the GitHub PR UI to produce a formal review. Manual UI seeding is the fallback specifically when no formal review exists to read the id from; then use the mutation for every subsequent re-request. **Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review. @@ -470,10 +83,12 @@ gh api repos/ptr727/Utilities/issues//comments --jq \ '[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}' ``` -Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes. +Coverage is confirmed when (1) exits 0 - **a formal review with no inline comments still satisfies path (1)**, because coverage is about the head SHA, not the comment count. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes. ### Bounded Retry Workflow +This path is only for a **genuinely missing** review - no Copilot review (formal *or* issue comment) covers the current head SHA after polling. A review that covered the head but produced no comments is a clean pass, not a missing review; do not enter this retry path for it. + If a review did not run on the current head, retry: 1. Wait briefly and check head-SHA coverage (see above). @@ -527,7 +142,13 @@ Issue-level Copilot comments (those in `issues//comments`) have no resolution Reply-body conventions: - Accepted bug/style fix: include fixing commit SHA and a one-line summary. -- Declined style comment: cite the rule (AGENTS.md or language CODESTYLE) and the existing-tree precedent. +- Declined style comment: cite the rule (AGENTS.md or the CODESTYLE.md language section) and the existing-tree precedent. - Declined architecture proposal: one-sentence rationale. After the final push, sweep-resolve stale older threads for removed code paths. + +## When in Doubt + +Read [AGENTS.md](../AGENTS.md) for this repo's conventions. For code-style rules, [`CODESTYLE.md`](../CODESTYLE.md) (its General section plus the .NET section) is authoritative. Don't restate any of these files' rules in commit bodies or PR descriptions - keep those focused on the change itself. + +**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream). diff --git a/.github/workflows/build-nugetlibrary-task.yml b/.github/workflows/build-nugetlibrary-task.yml index 5351e18..5a8a9dd 100644 --- a/.github/workflows/build-nugetlibrary-task.yml +++ b/.github/workflows/build-nugetlibrary-task.yml @@ -23,6 +23,13 @@ on: branch: required: true type: string + # Smoke mode: build for validation only and skip the artifact zip/upload. A PR + # smoke run has no consumer for the artifact (the github-release job is gated + # `!smoke`), so uploading it just burns artifact storage. + smoke: + required: false + type: boolean + default: false outputs: # Output of the uploaded artifact id artifact-id: @@ -52,7 +59,7 @@ jobs: dotnet-version: 10.x - name: Checkout code step - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ inputs.ref }} @@ -79,6 +86,7 @@ jobs: --skip-duplicate - name: Zip output step + if: ${{ !inputs.smoke }} run: | set -euo pipefail 7z a -t7z ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} ${{ runner.temp }}/publish/* @@ -86,8 +94,10 @@ jobs: # Branch-suffixed so the publisher's branch matrix can build both # branches in one run without colliding on the artifact name. - name: Upload build artifacts step + if: ${{ !inputs.smoke }} id: artifact-upload-step uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: nugetlibrary-build-${{ inputs.branch }} path: ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} + retention-days: 1 diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index 094e193..ea77f83 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -63,8 +63,10 @@ jobs: # commit than the one the release tag (also GitCommitId) points at. ref: ${{ needs.get-version.outputs.GitCommitId }} branch: ${{ inputs.branch }} - # Conditional push to NuGet.org — never on a smoke build. + # Conditional push to NuGet.org - never on a smoke build. push: ${{ inputs.nuget && !inputs.smoke }} + # Skip the release-asset upload on smoke (nothing consumes it on a PR). + smoke: ${{ inputs.smoke }} github-release: name: Publish GitHub release job @@ -81,7 +83,7 @@ jobs: # possibly-moving `inputs.ref` branch, so the uploaded release files # match the tag even if the branch advances mid-run. - name: Checkout code step - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ needs.get-version.outputs.GitCommitId }} @@ -95,7 +97,7 @@ jobs: # NBGV can produce a SemVer2 that was already released. GitHub release # creation has no built-in skip-duplicate (unlike NuGet's # `--skip-duplicate`), and re-publishing an unchanged version is exactly - # the churn the two-phase model avoids — so skip the release step when a + # the churn the two-phase model avoids - so skip the release step when a # release for this tag already exists. - name: Check for existing release step id: release-exists @@ -118,7 +120,7 @@ jobs: # `target_commitish` MUST be set explicitly: softprops doesn't pass a # default through, and GitHub's REST API then defaults the new tag to # the repository's default branch (main). We pin it to NBGV's - # `GitCommitId` — the exact commit the version was computed from. This + # `GitCommitId` - the exact commit the version was computed from. This # avoids two bugs: `github.sha` would be wrong (the publisher's branch # matrix builds `develop` from a run whose `github.sha` is main's tip), # and `inputs.branch` would be a moving ref (a commit landing mid-run @@ -128,7 +130,7 @@ jobs: # partially-created release for the same tag. - name: Create GitHub release step if: ${{ steps.release-exists.outputs.exists == 'false' || github.event_name == 'workflow_dispatch' }} - uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1 with: generate_release_notes: true tag_name: ${{ needs.get-version.outputs.SemVer2 }} diff --git a/.github/workflows/get-version-task.yml b/.github/workflows/get-version-task.yml index 259aa52..3fb12ad 100644 --- a/.github/workflows/get-version-task.yml +++ b/.github/workflows/get-version-task.yml @@ -48,7 +48,7 @@ jobs: dotnet-version: 10.x - name: Checkout code step - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ inputs.ref }} fetch-depth: 0 diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 934e986..090b2b0 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -63,7 +63,7 @@ jobs: dotnet-version: 10.x - name: Checkout code step - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 # `dotnet husky run` is the repo's git-hook runner; it invokes the same # CSharpier + dotnet format style checks the build conventions require. diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8a267a1..f82fbfa 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,26 +12,39 @@ { "version": "2.0.0", "tasks": [ + // The first three tasks are the .NET clean-compile set (CODESTYLE.md) + // carried verbatim; the rest are convenience/project-specific. { "label": ".NET Build", - "type": "dotnet", - "task": "build", + "type": "process", + "command": "dotnet", + "args": [ + "build", + "${workspaceFolder}", + "--verbosity=diagnostic" + ], "group": "build", - "problemMatcher": ["$msCompile"], + "problemMatcher": [ + "$msCompile" + ], "presentation": { "showReuseMessage": false, "clear": false } }, { - "label": ".NET Publish", + "label": "CSharpier Format", "type": "process", "command": "dotnet", "args": [ - "publish", - "${workspaceFolder}/Utilities/Utilities.csproj" + "csharpier", + "format", + "--log-level=debug", + "." + ], + "problemMatcher": [ + "$msCompile" ], - "problemMatcher": ["$msCompile"], "presentation": { "showReuseMessage": false, "clear": false @@ -43,6 +56,7 @@ "command": "dotnet", "args": [ "format", + "style", "--verify-no-changes", "--severity=info", "--verbosity=detailed" @@ -53,17 +67,22 @@ "presentation": { "showReuseMessage": false, "clear": false - } + }, + "dependsOrder": "sequence", + "dependsOn": [ + "CSharpier Format", + ".NET Build" + ] }, + // Convenience / project-specific tasks (adapt or drop per repo). { - "label": "CSharpier Format", + "label": ".NET Tool Update", "type": "process", "command": "dotnet", "args": [ - "csharpier", - "format", - "--log-level=debug", - "." + "tool", + "update", + "--all" ], "problemMatcher": [ "$msCompile" @@ -74,13 +93,12 @@ } }, { - "label": ".Net Tool Update", + "label": ".NET Publish", "type": "process", "command": "dotnet", "args": [ - "tool", - "update", - "--all" + "publish", + "${workspaceFolder}/Utilities/Utilities.csproj" ], "problemMatcher": [ "$msCompile" @@ -105,6 +123,6 @@ "showReuseMessage": false, "clear": false } - }, - ] + } + ] } diff --git a/AGENTS.md b/AGENTS.md index c7101cb..7c42873 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,21 +1,23 @@ # Instructions for AI Coding Agents -**Utilities** is a C# .NET NuGet library (published as `InsaneGenius.Utilities`). The library ships under [`Utilities/`](./Utilities/), with a `Sandbox/` console app for experimentation and `UtilitiesTests/` for xUnit tests. This file is the cross-cutting source of truth for process rules; the C# style guidance lives in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md). +**Utilities** is a C# .NET NuGet library (published as `InsaneGenius.Utilities`). The library ships under [`Utilities/`](./Utilities/), with a `Sandbox/` console app for experimentation and `UtilitiesTests/` for xUnit tests. This file is the cross-cutting source of truth for process rules and this repo's project-specific conventions and public-API contracts; the code-style rules live in [`CODESTYLE.md`](./CODESTYLE.md) at the repo root - one guide with a General section that applies repo-wide plus a droppable .NET language section. This repo tracks the [ptr727/ProjectTemplate](https://github.com/ptr727/ProjectTemplate) two-phase release model. It is a **NuGet-only** derivation: it has no Docker, executable, PyPI, or codegen targets, so the template's `build-docker-task.yml`, `build-executable-task.yml`, `build-pypilibrary-task.yml`, and `run-codegen-*.yml` workflows are intentionally absent, and the merge-bot carries only the Dependabot path. Keep the remaining workflow filenames and structure aligned with the template so upstream changes apply as minimal deltas. ## Git and Commit Rules - **Default to staging, not committing.** Stage changes with `git add` and leave `git commit` to the developer unless explicitly authorized for the current task. Authorization is scope-bound to that task. +- **All commits must be cryptographically signed (SSH or GPG).** Branch protection enforces this on both branches; unsigned commits are rejected on push. Signing depends on environment configuration (`git config commit.gpgsign true`, a configured `user.signingkey`, and a working signing agent). If signing is not configured, **do not commit** - surface the missing config to the developer and stop at `git add`. - **Never force push** (`git push --force` / `--force-with-lease`) and **never run destructive git commands** (`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`) without explicit developer instruction. ## Branching Model -- `develop` is the integration branch. Feature branches → `develop` is **squash-only**; develop is kept linear. -- `develop` → `main` is **merge-commit only** (no squash, no rebase). Merge commits preserve develop's commit list as a real second-parent reference on main. -- **`develop` is forward-only — no `main → develop` back-merges.** Each branch absorbs its own Dependabot PRs directly. +- `develop` is the integration branch. Feature branches -> `develop` is **squash-only**; develop is kept linear. +- `develop` -> `main` is **merge-commit only** (no squash, no rebase). Merge commits preserve develop's commit list as a real second-parent reference on main. +- All commits on both branches must be cryptographically signed (SSH or GPG). Squash and merge commits created via the GitHub UI are signed by GitHub's web-flow key. +- **`develop` is forward-only - no `main -> develop` back-merges.** Each branch absorbs its own Dependabot PRs directly. - **Both branch rulesets intentionally omit "Require branches to be up to date before merging".** On `main` the graph-based check would fail on every release (main's new merge commit is never back-merged into develop); on `develop` it stalls bot auto-merge when two bot PRs land in the same window. -- **Dependabot targets both `main` and `develop` in parallel.** [`.github/dependabot.yml`](./.github/dependabot.yml) duplicates every ecosystem entry (one per branch). The merge-bot ([`.github/workflows/merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml)) dispatches `--squash` or `--merge` from each PR's base ref via a `case` statement so the form matches the ruleset on either base. Dependabot **security** PRs always open against the default branch (`main`) — the same `case` statement covers them. +- **Dependabot targets both `main` and `develop` in parallel.** [`.github/dependabot.yml`](./.github/dependabot.yml) duplicates every ecosystem entry (one per branch). The merge-bot ([`.github/workflows/merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml)) dispatches `--squash` or `--merge` from each PR's base ref via a `case` statement so the form matches the ruleset on either base. Dependabot **security** PRs always open against the default branch (`main`) - the same `case` statement covers them. - **Maintainer-pushed commits on a bot PR auto-disable auto-merge.** The merge-bot's `merge-dependabot` job only fires on `opened` / `reopened` (auto-merge is enabled once per PR); the `disable-auto-merge-on-maintainer-push` job disables it on a `synchronize` event whose actor isn't Dependabot. Re-enable manually when ready. - **App-token workflows use Client ID, not App ID.** `actions/create-github-app-token` deprecated the numeric `app-id` input in v3.0.0; use `client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }}`. @@ -25,34 +27,76 @@ The repo uses a **two-phase model by default**: PRs build fast, publishing is ba - **PRs smoke-test only.** [`test-pull-request.yml`](./.github/workflows/test-pull-request.yml) always runs unit tests, then a `dorny/paths-filter` `changes` job gates a smoke build of the library only when it changed (Debug for develop / Release for main), never publishing. - **Merges don't publish by default.** [`publish-release.yml`](./.github/workflows/publish-release.yml) is the sole publisher: its **weekly schedule** (Mondays 02:00 UTC) and **manual `workflow_dispatch`** always do the full build/publish of **both** `main` and `develop` (a branch matrix). Its `push` trigger publishes only when the **`PUBLISH_ON_MERGE` repository variable** is `true` (opt-in continuous-release). Unset/`false` = two-phase. -- **Idempotent weekly republish.** NBGV can produce the same `SemVer2` on an unchanged branch, so the GitHub release step is skipped when the tag already exists, and the NuGet push uses `--skip-duplicate` — an unchanged week is a no-op. +- **Idempotent weekly republish.** NBGV can produce the same `SemVer2` on an unchanged branch, so the GitHub release step is skipped when the tag already exists, and the NuGet push uses `--skip-duplicate` - an unchanged week is a no-op. - **Required check.** The `changes` job is in the `Check pull request workflow status` aggregator's `needs` and **must succeed** (not just "not fail") so a paths-filter error can never let a library-changing PR merge with its smoke build silently skipped. Skipped smoke jobs (no matching change) pass; `failure`/`cancelled` blocks. -- **Reusable-task parameter contract.** Every `build-*-task.yml` and `build-release-task.yml` takes `ref` (git ref to check out/version), `branch` (logical branch driving config/tags/prerelease — `main` ⇒ Release/non-prerelease, else Debug/prerelease), and where relevant `smoke`. **Branch-derived config keys off `inputs.branch`, never `github.ref_name`** — the publisher's matrix builds `develop` from a run whose `github.ref_name` is `main`. Artifact names are branch-suffixed so both matrix legs coexist in one run. +- **Reusable-task parameter contract.** Every `build-*-task.yml` and `build-release-task.yml` takes `ref` (git ref to check out/version), `branch` (logical branch driving config/tags/prerelease - `main` => Release/non-prerelease, else Debug/prerelease), and where relevant `smoke`. **Branch-derived config keys off `inputs.branch`, never `github.ref_name`** - the publisher's matrix builds `develop` from a run whose `github.ref_name` is `main`. Artifact names are branch-suffixed so both matrix legs coexist in one run. +- **Versioning is semantic and maintainer-controlled.** The `version` (major.minor) in [`version.json`](./version.json) is the version floor; NBGV appends the git height (the SemVer patch position) for the build version. `main` (the public release ref) builds a stable `X.Y.`; `develop` builds a prerelease `X.Y.-g`. The maintainer edits `version.json`; dependency bumps, CI/workflow fixes, doc edits, and template re-syncs leave it untouched. + - **Bump `version.json` only for functional changes, by maintainer instruction.** Raise the major/minor when the work being introduced warrants a new semantic version - a new feature, a behavior or API change, a breaking change - and do it in the PR that introduces that work (typically on `develop`). Do **not** bump on a fixed cadence or mechanically after a release. NBGV advances the patch (git height) on every commit automatically, so a release always gets a fresh build version without any `version.json` edit. + - **No post-release bump; no develop-ahead requirement.** NBGV advances the patch (git height) on every commit, so a release always gets a fresh build version with no `version.json` edit and there is no `bump-version-X.Y` PR after a release. A `develop -> main` promotion carries whatever `version.json` is current: a promotion with a functional bump releases that new version on `main`; a maintenance-only promotion carries the unchanged `version.json` and `main` advances only its NBGV height. ## Build Configuration - **Central Package Management.** Package versions live in [`Directory.Packages.props`](./Directory.Packages.props); shared build properties (target framework, analyzers, `TreatWarningsAsErrors`) live in [`Directory.Build.props`](./Directory.Build.props). Project files carry no `Version=` on ``. -- **Versioning.** Nerdbank.GitVersioning reads [`version.json`](./version.json); only `main` is a public release ref. Don't put release-bump magnitude in PR titles — NBGV computes the next version from git history. -- **Analyzer relaxations.** `Directory.Build.props` mirrors the template's strict `AnalysisLevel latest-all` / `AnalysisMode All` / `TreatWarningsAsErrors`. Because this is a pre-existing (brownfield) library, a specific set of rules that would otherwise break the build — or require breaking the published public API — are relaxed back to suggestion in [`.editorconfig`](./.editorconfig) (and `IL3058` via `NoWarn` in the AOT project files). Each relaxation is documented inline; prefer fixing new violations over adding new relaxations. +- **Versioning.** Nerdbank.GitVersioning reads [`version.json`](./version.json); only `main` is a public release ref. Don't put release-bump magnitude in PR titles - NBGV computes the next version from git history. +- **Analyzer relaxations.** `Directory.Build.props` mirrors the template's strict `AnalysisLevel latest-all` / `AnalysisMode All` / `TreatWarningsAsErrors`. Because this is a pre-existing (brownfield) library, a specific set of rules that would otherwise break the build - or require breaking the published public API - are relaxed back to suggestion in [`.editorconfig`](./.editorconfig) (and `IL3058` via `NoWarn` in the AOT project files). Each relaxation is documented inline; prefer fixing new violations over adding new relaxations. -## Workflow YAML Conventions +## Pull Request Title and Commit Message Conventions -- **Action pinning**: pin **every** action to a commit SHA with a trailing `# vX.Y.Z` comment. Documented exception: [`dotnet/nbgv`](./.github/workflows/get-version-task.yml) is consumed via `@master` because the upstream tag stream lags `master` and Dependabot would propose a downgrade. -- **Filename**: reusable workflows (`on: workflow_call`) end in `-task.yml`; entry-point workflows do not use the `-task` suffix. -- **Workflow `name:`**: reusable workflow names end in **"task"**; entry-point names end in **"action"**. -- **Job and step `name:`**: every job's `name:` ends in **"job"**; every step's `name:` ends in **"step"**. **Exception**: the ruleset-bound required-status-check job `Check pull request workflow status` in `test-pull-request.yml` keeps its name verbatim — renaming silently breaks required-status-check enforcement. -- **Concurrency**: top-level workflows use `group: '${{ github.workflow }}-${{ github.ref }}'`, `cancel-in-progress: true`. Documented exceptions: `merge-bot-pull-request.yml` (`cancel-in-progress: false`, to run enable/disable events to completion in arrival order) and `publish-release.yml` (global ref-independent group + `cancel-in-progress: false`, so scheduled and manual publishes serialize instead of double-publishing). -- **Shells**: multi-line bash `run:` blocks start with `set -euo pipefail`. -- **Conditionals**: multi-line `if:` uses folded scalar `if: >-`. -- **Tag pinning on releases**: pass `target_commitish` to `softprops/action-gh-release` explicitly, pinned to NBGV's `GitCommitId` (the exact built commit), not `github.sha` or a branch name. -- There is no CI workflow-lint job — lint workflow edits with `actionlint` locally before pushing. +### Format -## Pull Request Title and Commit Message Conventions +- Imperative subject summarizing the change, <=72 characters, no trailing period. ("Add async download overloads", not "Added X" or "Adds X".) +- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*. + +### Rules -- Imperative subject summarizing the change, ≤72 characters, no trailing period. -- Don't write vague titles (`update stuff`, `wip`). Dependabot's default `Bump X from Y to Z` titles are fine. +- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.) - Don't add `Co-Authored-By:` lines unless the developer explicitly asks. -- Use US English spelling. +- Don't put release-bump magnitude in the title - no "minor", "patch", "release v3.5", etc. Nerdbank.GitVersioning computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected. +- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from). + +### Examples + +```text +Add structured logging extensions to library +Pin softprops/action-gh-release to commit SHA +Drop ProcessEx wrapper in favor of CliWrap +Bump xunit.v3 from 3.2.2 to 3.3.0 +Clarify release model in README +``` + +## Documentation Style Conventions + +### Markdown + +- Use reference-style links for any URL referenced more than once or appearing in lists; alphabetize the reference definitions block. Inline single-use relative links (e.g. `[CODESTYLE.md](./CODESTYLE.md)`) are fine. +- One logical paragraph per line; no hard-wrap line-length limit. For an intentional hard line break within a block - stacked badges, status, or license lines - end the line with a trailing backslash (`\`); this explicit form is preferred over trailing whitespace and is not treated as a paragraph split. +- Headings follow the title-case-with-short-bind-words rule from the PR-title section. +- **Write docs in the current state, not as a change from a prior one.** Describe what *is*: "X does Y", never "X *now* does Y" or "changed/switched to Y". Before/after framing belongs in changelogs, commit messages, and PR descriptions - not in `README.md` or other living docs. + +### Comments + +Applies to code and workflow (`#`) comments alike. + +- Comment only when the code does not explain itself or the logic is genuinely complex. Self-evident code needs no comment. +- Write for the human reading *this* project's code now: state what the code does and only the non-obvious *why*. No cross-project references (do not name other repos), no historic or design narrative, no rule citations - governance lives in this file, not echoed inline. +- Match the surrounding code's line length (typically ~120), not an 80-column wrap. + +### Character Set + +- **Write ASCII in all agent-authored text** - documentation, code, comments, commit messages, and PR descriptions. Replace typographic Unicode with its ASCII equivalent on sight: + - em dash and en dash -> hyphen `-` (use a spaced ` - ` for an em-dash-style clause break) + - right arrow -> `->`; double arrow -> `=>`; `<=` and `>=` for the inequality symbols + - curly quotes -> straight `'` and `"`; ellipsis -> `...` +- **Allowed non-ASCII**: scientific/technical symbols with no clean ASCII equivalent (ohm, micro, degree, pi), and Unicode the developer deliberately typed (emoji callout markers in `README.md`). Preserve those; never strip the developer's own characters. + +### Line Endings + +- **[`.editorconfig`](./.editorconfig) defines the correct line ending per file type:** **CRLF** for `.md`, `.cs`, XML/`.csproj`/`.props`/`.targets`, `.yml`/`.yaml`, `.json`, and `.cmd`/`.bat`/`.ps1`; **LF** for `.sh`. `.gitattributes` is `* -text`, so git stores the exact bytes you commit and will **not** normalize endings for you. +- **New files:** create them with the `.editorconfig`-mandated ending. **Editing an existing file:** preserve the file's current line endings - do not reflow them as a side effect of a content change. After any programmatic edit, verify with `git diff --stat` that only the lines you changed are touched; if a diff balloons to the whole file, you flipped the endings - restore them and re-stage. + +### Quantitative Claims + +- Any quantitative claim in `README.md` (counts, sizes, version floors, supported platforms) must be verified against current code. If a doc number is derived from a code constant, mark the dependency in a source-code comment so the next editor knows to update both. ## PR Review Etiquette @@ -98,8 +142,82 @@ Bring the user in when: Anti-pattern: don't keep flipping the code on the same style point. Flip the rule once and stick to the rule. +## Workflow YAML Conventions + +- **Action pinning**: pin **every** action to a commit SHA with a trailing `# vX.Y.Z` comment. Documented exception: [`dotnet/nbgv`](./.github/workflows/get-version-task.yml) is consumed via `@master` because the upstream tag stream lags `master` and Dependabot would propose a downgrade. +- **Filename**: reusable workflows (`on: workflow_call`) end in `-task.yml`; entry-point workflows do not use the `-task` suffix. +- **Workflow `name:`**: reusable workflow names end in **"task"**; entry-point names end in **"action"**. +- **Job and step `name:`**: every job's `name:` ends in **"job"**; every step's `name:` ends in **"step"**. **Exception**: the ruleset-bound required-status-check job `Check pull request workflow status` in `test-pull-request.yml` keeps its name verbatim - renaming silently breaks required-status-check enforcement. +- **Concurrency**: top-level workflows use `group: '${{ github.workflow }}-${{ github.ref }}'`, `cancel-in-progress: true`. Documented exceptions: `merge-bot-pull-request.yml` (`cancel-in-progress: false`, to run enable/disable events to completion in arrival order) and `publish-release.yml` (global ref-independent group + `cancel-in-progress: false`, so scheduled and manual publishes serialize instead of double-publishing). +- **Shells**: multi-line bash `run:` blocks start with `set -euo pipefail`. +- **Conditionals**: multi-line `if:` uses folded scalar `if: >-`. +- **Artifact retention**: intermediate build artifacts (`actions/upload-artifact`) are consumed by a later job in the same run, so set `retention-days: 1` - the default 90-day retention otherwise piles up against the account-wide artifact-storage quota. The durable copies live on the GitHub release, not in workflow artifacts. +- **Tag pinning on releases**: pass `target_commitish` to `softprops/action-gh-release` explicitly, pinned to NBGV's `GitCommitId` (the exact built commit), not `github.sha` or a branch name. +- There is no CI workflow-lint job - lint workflow edits with `actionlint` locally before pushing. + +### Running the Linters Locally (Known-Working Invocations) + +There is no CI lint job for workflow YAML or Markdown - the gate is local. Prefer the Docker invocations below; they need no local toolchain and auto-discover their targets from the working directory. + +- **actionlint** (run after any `.github/workflows/` edit, since workflow-only changes are not smoke-built): + + ```sh + docker run --rm -v "$PWD":/repo --workdir /repo rhysd/actionlint:latest -color + ``` + + The `rhysd/actionlint` image bundles `shellcheck`, so it also validates `run:` shell blocks. + +- **markdownlint-cli2** (mirrors the davidanson VS Code extension via the shared [`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc)): + + ```sh + docker run --rm -v "$PWD":/workdir davidanson/markdownlint-cli2:latest "**/*.md" + ``` + +When pulling a public image fails on a Docker-Desktop/WSL credential-helper error (`docker-credential-desktop.exe: exec format error`), retry with an empty Docker config: `DOCKER_CONFIG=$(mktemp -d) docker run ...` after writing `{}` to `$DOCKER_CONFIG/config.json`. + +## Project Structure + +- **.NET projects** (build with `dotnet build`, test with `dotnet test`): + - `Utilities/` - the reusable .NET NuGet library (published as `InsaneGenius.Utilities`) + - `Sandbox/` - console app for experimentation + - `UtilitiesTests/` - xUnit tests + - **Style guide: [`CODESTYLE.md`](./CODESTYLE.md) ".NET" section**. +- **Cross-cutting**: + - `.github/` - workflows, Dependabot, Copilot instructions + - `.vscode/` - debug configs and tasks; the `.NET` clean-compile task group is carried verbatim (see [`CODESTYLE.md`](./CODESTYLE.md)) + +After editing code, the `.NET` clean-compile (the `.NET Format` task) must pass before commit, and brownfield status never licenses relaxing analyzer severities or silencing newly surfaced diagnostics - both rules live in [`CODESTYLE.md`](./CODESTYLE.md) "General". + +## Library API Conventions + +Project-specific public-API conventions for the library (these are behavioral contracts, so they live here rather than in `CODESTYLE.md`): + +- **I/O methods return `bool`** for success/failure; additional outputs use `out` parameters. +- **Async methods carry the `Async` suffix** and an optional `CancellationToken cancellationToken = default`, passed through to the underlying call. +- **`Download`** reuses a thread-safe `Lazy` and uses `HttpCompletionOption.ResponseHeadersRead`; async overloads return tuples for multiple values. +- **`FileEx`** wraps I/O in retry logic configured via `Options`, with cancellation via `Options.Cancel` and the method parameter. +- **`StringCompression`** uses Deflate, supports configurable compression levels, and passes `leaveOpen` so the caller retains stream ownership. +- **`Extensions`** uses the C# `extension` syntax (inside a static class) for logger and string helpers. + +## Files and Sections Derived Repos Must Carry Verbatim + +These artifacts are the template's cross-cutting contract; this repo carries each of them. Re-sync them from the template when it changes, adapting only the noted placeholders. + +- **[`AGENTS.md`](./AGENTS.md) "PR Review Etiquette" section** - the provider-agnostic review-loop contract. Carried verbatim (it names no owner/repo). +- **[`.github/copilot-instructions.md`](./.github/copilot-instructions.md)** - the whole file is a drop-in; its "GitHub Copilot Review Runbook" carries the provider mechanics. Only the `` / `` placeholders are adapted (to `ptr727` / `Utilities`). Keep it **narrow** - provider mechanics plus the inline commit/PR-title summary; project-specific conventions and API contracts belong in this file instead. +- **[`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc)** - the shared lint config read by both the davidanson `markdownlint` IDE extension and CLI `markdownlint-cli2`, so the IDE and command line stay in lock-step. Carried verbatim (it is repo-agnostic). +- **[`.editorconfig`](./.editorconfig) and [`.gitattributes`](./.gitattributes)** - line-ending governance. The defaults + per-extension EOL block is always-verbatim; the `[*.cs]` + ReSharper style block at the end is .NET-only (the file marks the boundary). +- **[`CODESTYLE.md`](./CODESTYLE.md)** - the single code-style guide. Its **General** section is always carried; each language section is droppable (this repo keeps the .NET section and drops the template's Python section). **Repo-root placement is load-bearing** - `AGENTS.md` links it as `./CODESTYLE.md` and `.github/copilot-instructions.md` as `../CODESTYLE.md`, so moving it breaks those links. Adapt the in-section repo-specific bits: the .NET project-folder list, the `InternalsVisibleTo` project names, and the VS Code task labels. +- **[`.vscode/tasks.json`](./.vscode/tasks.json)** - carry the **named clean-compile definitions verbatim**: the `.NET Build`, `CSharpier Format`, and `.NET Format` tasks. Their names are owned by the `CODESTYLE.md` ".NET" section and their command sequence + arguments are the canonical clean-compile spec. Convenience tasks (`.NET Tool Update`, `.NET Publish`, `Husky.Net Run`) are the adapt zone. + +## Staying in Sync and Reporting Drift Upstream + +This repo re-syncs against [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) periodically, not just at creation: pull the current version of each verbatim-carry artifact above and re-apply it (adapting only the noted placeholders). For [`CODESTYLE.md`](./CODESTYLE.md), re-sync the whole file from the template and then drop the language section(s) this repo doesn't ship (always keeping the General section) - replacing the file wholesale and trimming whole sections is simpler to keep current than hand-editing snippets. + +**Drift flows back upstream as an issue, not a private fix.** When re-syncing, if you find a discrepancy that should be fixed in the **template itself** - a gap, an outdated instruction, a missing rule, something that bit this repo and would bite the next derived repo too - **open an issue in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate)** describing it, rather than only patching it locally. A local fix realigns *this* repo; an upstream issue (then fix) corrects it for every future derived repo and keeps the template the single source of truth. This upstream-issue rule is this repo's sole cross-repo obligation: do not name sibling or downstream repos in this repo's docs, comments, or AGENTS - a reader here cares only about this project. + ## Maintainer Setup (GitHub) -- **Secrets**: `NUGET_API_KEY` (NuGet.org push); `CODEGEN_APP_CLIENT_ID` + `CODEGEN_APP_PRIVATE_KEY` for the merge-bot's GitHub App token — add these to **both** the Actions and Dependabot secret stores. -- **Repository variable**: `PUBLISH_ON_MERGE` — leave unset for the two-phase model; set to `true` for continuous-release. +- **Secrets**: `NUGET_API_KEY` (NuGet.org push); `CODEGEN_APP_CLIENT_ID` + `CODEGEN_APP_PRIVATE_KEY` for the merge-bot's GitHub App token - add these to **both** the Actions and Dependabot secret stores. +- **Repository variable**: `PUBLISH_ON_MERGE` - leave unset for the two-phase model; set to `true` for continuous-release. - **Rulesets**: `develop` squash-only, `main` merge-only; both require the `Check pull request workflow status` check and signed commits; both omit "Require branches to be up to date before merging". diff --git a/CODESTYLE.md b/CODESTYLE.md new file mode 100644 index 0000000..a22b877 --- /dev/null +++ b/CODESTYLE.md @@ -0,0 +1,329 @@ +# Code Style and Formatting Rules + +This is the single code-style guide for the repo. The **General** section applies repo-wide and is always carried. The **.NET** section is the language section for this repo's C# projects, self-contained like the `.editorconfig` `[*.cs]` block. (The upstream [ProjectTemplate](https://github.com/ptr727/ProjectTemplate) also ships a Python section; this NuGet-only repo drops it.) + +Cross-cutting *process* rules (PR titles, branching, US English, markdown style, comments philosophy, workflow YAML, PR review etiquette) live in [AGENTS.md](./AGENTS.md) and are not repeated here. + +## General + +These rules apply to every language in the repo. + +### Tooling Names and Casing + +Use each tool's official casing in task labels, docs, and prose - `.NET` (not `.Net`), `CSharpier`. Don't invent personal variants. + +### Clean-Compile Verification + +Each language defines a **clean-compile** verification - the combination of build, formatter, linter, and code-analysis tools that must report clean before a commit. It is exposed as one or more **named** VS Code tasks (or, where a language ships no tasks, documented commands), and those definitions are **carried verbatim** across derived repos. The concrete names live in the language section below. + +- **Run it after every code change.** The clean-compile must pass before you commit; CI runs the same checks as a backstop, and this repo's Husky.Net pre-commit hook runs them locally too. +- **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly; either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). + +### Analyzer Diagnostics and Suppressions + +- **A new port is not a license to silence diagnostics.** Brownfield / just-ported status never justifies relaxing analyzer or linter severities or muting newly surfaced warnings - fix them. (The only brownfield allowance in this template is the one-time git-signing / line-ending migration described in [AGENTS.md](./AGENTS.md) and [README.md](./README.md), which has nothing to do with code analysis.) +- **Suppress only genuine false-positives or deliberate, documented exceptions**, always at the **narrowest scope that fits**, in this order of preference: + 1. An **in-code annotation on the specific symbol**, with a justification - the language's attribute/comment form, never a blanket pragma spanning a region. + 2. The **owning project's local config** when the exception is project-wide for one project (e.g. a test project's own `.editorconfig`). + 3. The **root / shared config** only when the suppression is genuinely applicable to **every** project in the repo. +- **Never blanket-relax a batch of rules project-wide** to get a port to build. The per-language mechanics (which attribute, which config key) are in the language section. + +### Markdown and Spelling + +These apply repo-wide, in every directory: + +1. **Markdown linting**: All `.md` files must be lint-clean (error and warning free) via the VS Code `markdownlint` extension. [`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc) at the repo root is the single source of truth - the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules it deliberately disables (e.g. `MD013` line-length, `MD033` inline HTML) are **intentional** - do not "fix" them. This file is carried verbatim by every derived repo (see [AGENTS.md "Files and Sections Derived Repos Must Carry Verbatim"](./AGENTS.md#files-and-sections-derived-repos-must-carry-verbatim)). Fix violations at the source rather than disabling rules. +2. **Spelling**: All spelling must be clean via the CSpell VS Code integration; words must be correctly spelled in **US English** (the repo-wide convention - see [AGENTS.md](./AGENTS.md)). Project-specific terms go in the workspace CSpell config. + +## .NET + +This is the style guide for the .NET projects in this repo: [`Utilities/`](./Utilities/) (the published library), [`Sandbox/`](./Sandbox/) (a console app for experimentation), and [`UtilitiesTests/`](./UtilitiesTests/) (xUnit tests). + +### Build Requirements + +#### Zero Warnings Policy + +**CRITICAL**: All builds must complete without warnings. The project enforces this through: + +1. **The `.NET Format` clean-compile task** (see [Clean-Compile Verification](#clean-compile-verification)) + - The .NET clean-compile is the **`.NET Format`** VS Code task, which chains `CSharpier Format` -> `.NET Build` -> `dotnet format style --verify-no-changes`. These three task definitions are carried verbatim in [`.vscode/tasks.json`](./.vscode/tasks.json). + - After any code change it must pass before commit. Run the `.NET Format` task. To run it natively instead, reproduce that task chain from [`.vscode/tasks.json`](./.vscode/tasks.json) exactly - `CSharpier Format`, then `.NET Build`, then the `dotnet format style --verify-no-changes --severity=info ...` verify - without dropping or loosening any argument (tasks.json is the canonical command spec). Bare `dotnet format` alone, skipping CSharpier or the build, is not sufficient. + +2. **Analyzer configuration** (in [`Directory.Build.props`](./Directory.Build.props)) + - `latest-all` + - `All` + - `true` + - `true` - all warnings must be addressed, not relaxed; see [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions). + +3. **Pre-commit hooks** + - Husky.Net pre-commit hooks are wired in this repo (`.husky/` ships) and run CSharpier and `dotnet format` on every commit; commits are rejected if formatting fails. + +#### Build Tasks + +Available VS Code tasks (run them from VS Code's task runner - **Terminal -> Run Task** - or an agent's task-running tool). The first three are the clean-compile set, carried verbatim; the rest are convenience/project-specific tasks: + +- `.NET Build`: Build with diagnostic verbosity *(clean-compile)* +- `CSharpier Format`: Auto-format code with CSharpier *(clean-compile)* +- `.NET Format`: Run CSharpier and build, then verify formatting and style with `--verify-no-changes` *(clean-compile; the task to run after edits)* +- `.NET Tool Update`: Update dotnet tools *(convenience)* +- `.NET Publish`: Publish the library, exercising AOT *(project-specific)* +- `Husky.Net Run`: Run the pre-commit hook tasks on demand *(convenience)* + +### Tooling and Editor + +#### Code Formatting and Tooling + +1. **CSharpier**: Primary code formatter + - Invoked by the `CSharpier Format` task / `dotnet csharpier format --log-level=debug .` +2. **dotnet format**: Style verification + - Verify no changes: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed` +3. **Other tools** + - Husky.Net: pre-commit git-hook runner (installed as a dotnet tool) + - Nerdbank.GitVersioning: Version management + +Restore the tools with `dotnet tool restore` before the first commit. + +#### Editor Baseline + +1. **Required VS Code extensions**: CSharpier, EditorConfig, markdownlint, CSpell +2. **VS Code settings**: Use the workspace settings without overrides + +### Coding Standards and Conventions + +Note: Code snippets are illustrative examples only. Replace namespaces/types to match the project. + +#### C# Language Features + +1. **File-scoped namespaces** + + ```csharp + namespace InsaneGenius.Utilities; + ``` + +2. **Nullable reference types**: Enabled (`enable`) + - Use nullable annotations appropriately + - Use `required` for mandatory properties + +3. **Modern C# features**: Prefer modern language constructs + - Primary constructors when appropriate + - Pattern matching over traditional checks + - Collection expressions when types loosely match + - Extension methods using the `extension` syntax (inside a static class) + - Implicit object creation when type is apparent + - Range and index operators + +4. **Expression-bodied members**: Use for applicable members + - Methods, properties, accessors, operators, lambdas, local functions + +5. **`var` keyword**: Do NOT use `var` (always use explicit types) + + ```csharp + // Correct + int count = 42; + string name = "test"; + + // Incorrect + var count = 42; + var name = "test"; + ``` + +#### Naming Conventions + +1. **Private fields**: underscore prefix with camelCase + + ```csharp + private readonly HttpClient _httpClient; + private int _counter; + ``` + +2. **Static fields**: `s_` prefix with camelCase + + ```csharp + private static int s_instanceCount; + ``` + +3. **Constants**: PascalCase + + ```csharp + private const int MaxRetries = 3; + ``` + +#### Code Structure + +1. **Global usings**: Use `GlobalUsings.cs` for common namespaces + + ```csharp + global using System; + global using System.Net.Http; + global using System.Threading.Tasks; + global using Serilog; + ``` + +2. **Usings placement**: Outside namespace, sorted with `System` directives first + +3. **Braces**: Allman style + + ```csharp + public void Method() + { + if (condition) + { + // code + } + } + ``` + +4. **Indentation** + - C# files: 4 spaces + - XML/csproj files: 2 spaces + - YAML files: 2 spaces + - JSON files: 4 spaces + +5. **Line endings** + - C#, XML, YAML, JSON, Windows scripts: CRLF + - Linux scripts (`.sh`): LF + +6. **`#region`**: Do not use regions. Prefer logical file/folder/namespace organization. +7. **Member ordering (StyleCop SA1201)**: const -> static readonly -> static fields -> instance readonly fields -> instance fields -> constructors -> public (events -> properties -> indexers -> methods -> operators) -> non-public in same order -> nested types + +#### Comments and Documentation + +1. **XML documentation** + - `true` + - Document all public surfaces. + - Single-line summaries, additional details in remarks, document input parameters, return values, exceptions, and add crefs + + ```csharp + /// + /// Example of a single line summary. + /// + /// + /// Additional important details about usage. + /// + /// + /// A that can be used to cancel the request. + /// + /// + /// A indicating success. + /// + /// + /// Thrown when a required argument is null. + /// + public async Task DoWorkAsync(CancellationToken cancellationToken) {} + ``` + +#### Analyzer Suppressions (.NET) + +Follow the scope hierarchy in [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions). .NET mechanics, narrowest first: + +- **Never use `#pragma warning disable`** to silence an analyzer. +- **Symbol-scoped**: a `[System.Diagnostics.CodeAnalysis.SuppressMessage(...)]` attribute with a `Justification`, on the specific member or type: + + ```csharp + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1034:Nested types should not be visible", + Justification = "https://github.com/dotnet/sdk/issues/51681" + )] + ``` + +- **Project-scoped** (e.g. the test project): a `dotnet_diagnostic..severity` entry in *that project's own* `.editorconfig`, with a comment explaining why. This repo's library exceptions live in [`Utilities/.editorconfig`](./Utilities/.editorconfig) and its test exceptions in [`UtilitiesTests/.editorconfig`](./UtilitiesTests/.editorconfig). +- **Repo-wide**: a `dotnet_diagnostic..severity` entry in the root `.editorconfig`, only when the rule is genuinely not applicable to any project. Relaxing a batch of `CA*` rules (or `dotnet_analyzer_diagnostic.severity`) to push a brownfield port through the build is exactly what this forbids. + +#### Error Handling and Logging + +1. **Serilog logging**: Use structured logging + + ```csharp + logger.Error(exception, "{Function}", function); + ``` + +2. **Library log configuration**: The library exposes logging configuration + - Provide options or settings to supply an `ILogger` + - Offer a global fallback logger for static usage when needed (`LogOptions.Logger`) + +3. **CallerMemberName**: Use for automatic function name tracking + + ```csharp + public bool LogAndPropagate( + Exception exception, + [CallerMemberName] string function = "unknown" + ) + ``` + +4. **Logger extensions**: Use `Extensions.cs` for logger and other extension methods + + ```csharp + extension(ILogger logger) + { + public bool LogAndPropagate(Exception exception, ...) { } + } + ``` + +5. **Exceptions**: Do not swallow exceptions; log and rethrow or translate to a domain-specific exception + +#### Code Patterns + +1. **Guard clauses**: Prefer early returns for validation; use `ArgumentNullException.ThrowIfNull()` for null checks +2. **Async all the way**: Avoid blocking calls (`.Result`, `.Wait()`, `.GetAwaiter().GetResult()`); use `async`/`await` +3. **Cancellation tokens**: Accept `CancellationToken cancellationToken = default` as the last parameter and pass it through +4. **ConfigureAwait**: In library code, use `ConfigureAwait(false)` unless context is required + - Do not call `ConfigureAwait(false)` in xUnit tests (see xUnit1030) +5. **Disposables**: Use `await using` for async disposables; prefer `using` declarations; pass `leaveOpen` where a caller owns the stream +6. **LINQ vs loops**: Use LINQ for clarity, loops for hot paths or allocations +7. **HTTP**: Reuse `HttpClient` via a thread-safe `Lazy`; avoid per-request instantiation; use `HttpCompletionOption.ResponseHeadersRead` for streaming downloads +8. **Collections**: Prefer `IReadOnlyList`/`IReadOnlyCollection` for public APIs +9. **Immutability**: Prefer immutable records and init-only setters; prefer immutable or frozen collections for read-only data +10. **Sealing classes**: Seal classes that are not designed for inheritance +11. **Thread safety**: Use `Lazy` for static thread-safe instantiation; use `Lock` (C# 13+) instead of `object` for locks; avoid static mutable state and document thread-safety guarantees +12. **Performance**: Use `Span`/`Memory` for performance-critical I/O; use `ValueTask` for async methods that may complete synchronously; use `StringBuilder` for string concatenation in loops + +#### Testing Conventions + +1. **Framework**: xUnit + + ```csharp + [Fact] + public void MethodName_Scenario_ExpectedBehavior() + { + // Arrange + int expected = 42; + + // Act + int actual = GetValue(); + + // Assert + Assert.Equal(expected, actual); + } + ``` + +2. **Organization**: Arrange-Act-Assert pattern +3. **Naming**: Descriptive names with underscores; test file names match the class under test with a `Tests` suffix +4. **Theory tests**: Use `[Theory]` with `[InlineData]` +5. **Async coverage**: Add tests for every new async method + +### Project Configuration + +1. **Target framework**: .NET 10.0 (`net10.0`) + +2. **AOT compatibility** + - `true` + - Avoid runtime reflection (not AOT-friendly); prefer source generators + - Be mindful of trim warnings; verify with `dotnet publish` + +3. **Assembly information** + - Use semantic versioning (Nerdbank.GitVersioning) + - Include SourceLink: `true` + - Embed untracked sources: `true` + +4. **Internal visibility**: Use `InternalsVisibleTo` for test access + + ```xml + + + + ``` + +### Best Practices + +1. **Code reviews**: All changes go through pull requests diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..97ec9ba --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,18 @@ +# Utilities + +Some useful and not so useful C# .NET utility classes. + +## Release History + +- v3.5: + - Re-synced the repository structure and agent documentation with the upstream ProjectTemplate: added this `HISTORY.md` and a `CODESTYLE.md` .NET style guide, narrowed `.github/copilot-instructions.md` to the Copilot review runbook, and refreshed `AGENTS.md` conventions. + - Corrected the versioning policy to bump `version.json` only for functional changes. + - Swapped the recommended Todo VS Code add-on from Todo Tree to Better Todo Tree. +- v3.4: + - .NET 10 and AOT support. + - Removed `ProcessEx` process wrapper classes, use [CliWrap](https://github.com/Tyrrrz/CliWrap) instead. + - Code cleanup with help from Copilot. +- v3.3: + - Language tags split out into a separate dedicated library. +- v3.2 and earlier: + - Utility classes for downloads, file and directory operations with retry logic, string compression, byte-size formatting, console helpers, and command-line parsing. diff --git a/README.md b/README.md index fae204a..5ea1cbc 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,16 @@ Code and Pipeline is on [GitHub](https://github.com/ptr727/Utilities)\ Packages published on [NuGet](https://www.nuget.org/packages/InsaneGenius.Utilities/)\ ![NuGet](https://img.shields.io/nuget/v/InsaneGenius.Utilities?logo=nuget) -## Version History - -- v3.4: - - .NET 10 and AOT support. - - Removed `ProcessEx` process wrapper classes, use [CliWrap](https://github.com/Tyrrrz/CliWrap) instead. - - Code cleanup with help from Copilot. -- v3.3: - - Language tags moved to a dedicated [repo](https://github.com/ptr727/LanguageTags). +## Release Notes + +**Version: 3.5**: + +**Summary**: + +- Repository structure and agent documentation follow the upstream ProjectTemplate. +- Better Todo Tree is the recommended Todo VS Code add-on. + +See [Release History](./HISTORY.md) for complete release notes and older versions. ## License diff --git a/Utilities.code-workspace b/Utilities.code-workspace index b58f2dd..d3a6110 100644 --- a/Utilities.code-workspace +++ b/Utilities.code-workspace @@ -1,31 +1,31 @@ { - "folders": [ - { - "path": "." - } - ], - "settings": { - "cSpell.words": [ - "csdevkit", - "davidanson", - "dotnettools", - "extlang", - "gruntfuggly", - "Jernej", - "macrolanguage", - "nbgv", - "Nerdbank", - "nupkg", - "Serilog", - "Simoncic", - "softprops", - "stefanzweifel", - "subtag", - "templating", - "triggerbuild", - "winget" - ], - "dotnet.defaultSolution": "Utilities.sln", + "folders": [ + { + "path": "." + } + ], + "settings": { + "cSpell.words": [ + "csdevkit", + "davidanson", + "dotnettools", + "extlang", + "fanaticpythoner", + "Jernej", + "macrolanguage", + "nbgv", + "Nerdbank", + "nupkg", + "Serilog", + "Simoncic", + "softprops", + "stefanzweifel", + "subtag", + "templating", + "triggerbuild", + "winget" + ], + "dotnet.defaultSolution": "Utilities.slnx", "files.trimTrailingWhitespace": true, "[markdown]": { "files.trimTrailingWhitespace": false @@ -43,15 +43,15 @@ "csharp.debug.symbolOptions.searchNuGetOrgSymbolServer": true, "csharp.debug.symbolOptions.searchMicrosoftSymbolServer": true, "files.encoding": "utf8" - }, - "extensions": { - "recommendations": [ + }, + "extensions": { + "recommendations": [ "davidanson.vscode-markdownlint", - "gruntfuggly.todo-tree", "ms-dotnettools.csdevkit", "streetsidesoftware.code-spell-checker", "editorconfig.editorconfig", - "csharpier.csharpier-vscode" - ] - } + "csharpier.csharpier-vscode", + "fanaticpythoner.better-todo-tree" + ] + } } diff --git a/Utilities/.editorconfig b/Utilities/.editorconfig index ba0d0e5..0ab9e9a 100644 --- a/Utilities/.editorconfig +++ b/Utilities/.editorconfig @@ -5,3 +5,23 @@ root = false # Allow Ex dotnet_diagnostic.CA1711.severity = none + +# Library-scoped analyzer exceptions. Each is a deliberate, documented decision +# for this published library, not a brownfield blanket-relax (see CODESTYLE.md +# "Analyzer Diagnostics and Suppressions"). +# CA1002: the published InsaneGenius.Utilities surface intentionally exposes +# List (FileEx.EnumerateDirectory, StringHistory.StringList); changing to +# Collection is a breaking API change. +dotnet_diagnostic.CA1002.severity = suggestion +# CA1024: Download.GetHttpClient() is intentionally a method, not a property. +dotnet_diagnostic.CA1024.severity = suggestion +# CA1034: nested types generated by the C# extension members in Extensions.cs. +dotnet_diagnostic.CA1034.severity = suggestion +# CA1054: Download URL parameters are intentionally string, not System.Uri. +dotnet_diagnostic.CA1054.severity = suggestion +# CA2007: await using / await foreach disposal sites; the awaited async calls +# already use ConfigureAwait(false) and a ConfiguredAsyncDisposable rewrite +# hurts readability. +dotnet_diagnostic.CA2007.severity = suggestion +# CA5394: Random is used for retry jitter and temp-name generation, not security. +dotnet_diagnostic.CA5394.severity = suggestion diff --git a/UtilitiesTests/.editorconfig b/UtilitiesTests/.editorconfig index 9acafa5..4601a6c 100644 --- a/UtilitiesTests/.editorconfig +++ b/UtilitiesTests/.editorconfig @@ -8,3 +8,25 @@ dotnet_diagnostic.CA1707.severity = none # Ignore unused private members dotnet_diagnostic.IDE0052.severity = none + +# Test-scoped analyzer exceptions: rules that target production-code concerns +# and don't apply to xUnit test code. Each is documented, not a brownfield +# blanket-relax (see CODESTYLE.md "Analyzer Diagnostics and Suppressions"). +# CA1063: test IDisposable implementations are intentionally simple. +dotnet_diagnostic.CA1063.severity = suggestion +# CA1307: test string operations use the default comparison intentionally. +dotnet_diagnostic.CA1307.severity = suggestion +# CA1515: xUnit requires public test classes, so they can't be made internal. +dotnet_diagnostic.CA1515.severity = suggestion +# CA1823: xUnit fixture fields are injected for lifetime/collection wiring and +# are not always referenced directly. +dotnet_diagnostic.CA1823.severity = suggestion +# CA1849: synchronous calls inside async test paths are kept intentionally. +dotnet_diagnostic.CA1849.severity = suggestion +# CA2000: test disposable ownership is transferred or scoped to the test, so +# scope-based disposal analysis reports false positives. +dotnet_diagnostic.CA2000.severity = suggestion +# CA2007: ConfigureAwait(false) is not used in xUnit tests (see xUnit1030). +dotnet_diagnostic.CA2007.severity = suggestion +# CA5394: Random in tests is for test data, not security. +dotnet_diagnostic.CA5394.severity = suggestion diff --git a/version.json b/version.json index daa24fd..9a73362 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "3.4", + "version": "3.5", "publicReleaseRefSpec": [ "^refs/heads/main$" ],