From 44411059048741304bc90c6d313aeb0320c9ed74 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 8 Jun 2026 12:08:40 -0700 Subject: [PATCH 1/5] Bump version to 3.5 for next prerelease cycle (#334) Isolated version bump establishing the develop-leads invariant: raises `develop`'s minor from `3.4` to `3.5` so develop's NBGV prereleases sort above main's last stable `3.4.x`. Standalone version-only change per the versioning policy. --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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$" ], From 1aa8876ffa531cdcd09dc7f8f92a1a0d1c26bff4 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 8 Jun 2026 12:08:48 -0700 Subject: [PATCH 2/5] Document the develop-leads versioning policy (#333) Propagates the versioning rule from ptr727/ProjectTemplate: `develop` leads `main` by a minor (after a `develop -> main` release, bump develop's minor via an isolated `bump-version-X.Y` PR so develop's prereleases sort above main's stable), and maintenance promotions hold main's version. Added to AGENTS.md "Release Model" and `.github/copilot-instructions.md". (The README is the consumer-facing package readme with no dev-workflow section, so the rule lives in the agent-facing docs.) --- .github/copilot-instructions.md | 4 ++++ AGENTS.md | 21 +++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5f39aaf..5bfa2b0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -139,6 +139,10 @@ Use: `dotnet test` ## Commit Guidelines +### Versioning + +`develop` leads `main` by a minor. After a `develop -> main` release lands and main's publish completes, bump the minor in [version.json](../version.json) on `develop` via an isolated `bump-version-X.Y` PR (X.Y = the new minor, e.g. `bump-version-3.5`; this version-setting PR is the one place a version belongs in a PR title), so develop's NBGV prereleases sort above main's last stable. A `develop -> main` promotion that carries only maintenance (dependency bumps, CI/doc fixes, template re-syncs) holds main's version instead - `git checkout main -- version.json` on the promotion branch. See [AGENTS.md "Release Model"](../AGENTS.md#release-model). + ### Pre-Commit Process (Automated by Husky.Net) The following happens automatically on every commit: diff --git a/AGENTS.md b/AGENTS.md index c7101cb..f7a860c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,9 +13,9 @@ This repo tracks the [ptr727/ProjectTemplate](https://github.com/ptr727/ProjectT - `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 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,27 +25,28 @@ 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). `main` builds a stable `X.Y.`; `develop` builds a prerelease `X.Y.-g`. **`develop` leads `main` by a minor:** after a `develop -> main` release lands and main's publish completes, bump the minor in `version.json` on `develop` in an isolated `bump-version-X.Y` PR (X.Y = the new minor, e.g. `bump-version-3.5`; this version-setting PR is the one place a version belongs in a PR title), so develop's prereleases sort above main's last stable (at a shared `X.Y`, develop's `X.Y.-g` sorts *below* main's `X.Y.`). A **maintenance** `develop -> main` promotion (dependency bumps, CI/doc fixes, template re-syncs) holds main's version - `git checkout main -- version.json` on the promotion branch - so `main` advances only its height, not its minor. ## 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 - **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. +- **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. +- There is no CI workflow-lint job - lint workflow edits with `actionlint` locally before pushing. ## Pull Request Title and Commit Message Conventions @@ -100,6 +101,6 @@ Anti-pattern: don't keep flipping the code on the same style point. Flip the rul ## 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". From 800e37d56302e583c206f7f03a41ad9267fbf337 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:54:36 +0000 Subject: [PATCH 3/5] Bump actions/checkout from 6.0.3 to 7.0.0 in the actions-deps group (#335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions-deps group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 6.0.3 to 7.0.0
Release notes

Sourced from actions/checkout's releases.

v7.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v6.0.3...v7.0.0

Changelog

Sourced from actions/checkout's changelog.

Changelog

v7.0.0

v6.0.3

v6.0.2

v6.0.1

v6.0.0

v5.0.1

v5.0.0

v4.3.1

v4.3.0

v4.2.2

v4.2.1

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6.0.3&new-version=7.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-nugetlibrary-task.yml | 2 +- .github/workflows/build-release-task.yml | 2 +- .github/workflows/get-version-task.yml | 2 +- .github/workflows/test-pull-request.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-nugetlibrary-task.yml b/.github/workflows/build-nugetlibrary-task.yml index 5351e18..16197f6 100644 --- a/.github/workflows/build-nugetlibrary-task.yml +++ b/.github/workflows/build-nugetlibrary-task.yml @@ -52,7 +52,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 }} diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index 094e193..4e467d9 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -81,7 +81,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 }} 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. From 294226ad0a34c0d35cd876a888742a15362cd5a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:53:52 +0000 Subject: [PATCH 4/5] Bump softprops/action-gh-release from 3.0.0 to 3.0.1 in the actions-deps group (#337) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the actions-deps group with 1 update: [softprops/action-gh-release](https://github.com/softprops/action-gh-release). Updates `softprops/action-gh-release` from 3.0.0 to 3.0.1
Release notes

Sourced from softprops/action-gh-release's releases.

v3.0.1

3.0.1

  • maintenance release with updated dependencies
Changelog

Sourced from softprops/action-gh-release's changelog.

3.0.1

  • maintenance release with updated dependencies

3.0.0

3.0.0 is a major release that moves the action runtime from Node 20 to Node 24. Use v3 on GitHub-hosted runners and self-hosted fleets that already support the Node 24 Actions runtime. If you still need the last Node 20-compatible line, stay on v2.6.2.

What's Changed

Other Changes 🔄

  • Move the action runtime and bundle target to Node 24
  • Update @types/node to the Node 24 line and allow future Dependabot updates
  • Keep the floating major tag on v3; v2 remains pinned to the latest 2.x release

2.6.2

What's Changed

Other Changes 🔄

2.6.1

2.6.1 is a patch release focused on restoring linked discussion thread creation when discussion_category_name is set. It fixes [#764](https://github.com/softprops/action-gh-release/issues/764), where the draft-first publish flow stopped carrying the discussion category through the final publish step.

If you still hit an issue after upgrading, please open a report with the bug template and include a minimal repro or sanitized workflow snippet where possible.

What's Changed

Bug fixes 🐛

2.6.0

2.6.0 is a minor release centered on previous_tag support for generate_release_notes, which lets workflows pin GitHub's comparison base explicitly instead of relying on the default range. It also includes the recent concurrent asset upload recovery fix, a working_directory docs sync, a checked-bundle freshness guard for maintainers, and clearer immutable-prerelease guidance where GitHub platform behavior imposes constraints on how prerelease asset uploads can be published.

... (truncated)

Commits
  • 718ea10 release 3.0.1
  • f1a938b chore(deps): bump esbuild from 0.28.0 to 0.28.1 (#802)
  • 0066ead chore(deps): bump vite from 8.0.14 to 8.0.16 (#806)
  • dc643ca chore(deps): bump the npm group with 3 updates (#805)
  • 85ee99b chore(deps): bump actions/checkout in the github-actions group (#804)
  • 9ed3cf9 chore(deps): bump the npm group with 2 updates (#800)
  • 3efcac8 chore(deps): bump the npm group with 3 updates (#798)
  • 05d6b91 chore(deps): bump brace-expansion from 5.0.5 to 5.0.6 (#797)
  • 403a524 chore(deps): bump @​types/node from 24.12.2 to 24.12.3 in the npm group (#796)
  • 437e073 chore(deps): bump the npm group with 4 updates (#792)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=softprops/action-gh-release&package-manager=github_actions&previous-version=3.0.0&new-version=3.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-release-task.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index 4e467d9..d5ad95a 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -128,7 +128,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 }} From c90cc0a98821fd7e1d0333a4dee8a79e081214dd Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 22 Jun 2026 10:34:59 -0700 Subject: [PATCH 5/5] Re-sync from ProjectTemplate and swap Todo VS Code add-on (#340) Reconverge with upstream ProjectTemplate: add HISTORY.md and the consolidated CODESTYLE.md, narrow copilot-instructions.md, restructure AGENTS.md (corrected versioning, documentation-style, verbatim-carry, upstream-sync sections), mark the .editorconfig .NET-only boundary, re-scope brownfield CA relaxations into project-local configs, govern .vscode/tasks.json, smoke-gate the nuget build upload, README Release Notes summary, and swap the Todo add-on. Co-Authored-By: Claude Opus 4.8 (1M context) --- .editorconfig | 45 +- .github/copilot-instructions.md | 441 ++---------------- .github/workflows/build-nugetlibrary-task.yml | 10 + .github/workflows/build-release-task.yml | 8 +- .vscode/tasks.json | 56 ++- AGENTS.md | 157 ++++++- CODESTYLE.md | 329 +++++++++++++ HISTORY.md | 18 + README.md | 18 +- Utilities.code-workspace | 68 +-- Utilities/.editorconfig | 20 + UtilitiesTests/.editorconfig | 22 + 12 files changed, 658 insertions(+), 534 deletions(-) create mode 100644 CODESTYLE.md create mode 100644 HISTORY.md 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 5bfa2b0..da1fc8a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,427 +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 - -### Versioning - -`develop` leads `main` by a minor. After a `develop -> main` release lands and main's publish completes, bump the minor in [version.json](../version.json) on `develop` via an isolated `bump-version-X.Y` PR (X.Y = the new minor, e.g. `bump-version-3.5`; this version-setting PR is the one place a version belongs in a PR title), so develop's NBGV prereleases sort above main's last stable. A `develop -> main` promotion that carries only maintenance (dependency bumps, CI/doc fixes, template re-syncs) holds main's version instead - `git checkout main -- version.json` on the promotion branch. See [AGENTS.md "Release Model"](../AGENTS.md#release-model). - -### 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 @@ -447,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. @@ -474,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). @@ -531,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 16197f6..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: @@ -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 d5ad95a..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 @@ -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 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 f7a860c..7c42873 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,19 +1,21 @@ # 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. - **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. @@ -27,8 +29,10 @@ The repo uses a **two-phase model by default**: PRs build fast, publishing is ba - **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. - **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. -- **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). `main` builds a stable `X.Y.`; `develop` builds a prerelease `X.Y.-g`. **`develop` leads `main` by a minor:** after a `develop -> main` release lands and main's publish completes, bump the minor in `version.json` on `develop` in an isolated `bump-version-X.Y` PR (X.Y = the new minor, e.g. `bump-version-3.5`; this version-setting PR is the one place a version belongs in a PR title), so develop's prereleases sort above main's last stable (at a shared `X.Y`, develop's `X.Y.-g` sorts *below* main's `X.Y.`). A **maintenance** `develop -> main` promotion (dependency bumps, CI/doc fixes, template re-syncs) holds main's version - `git checkout main -- version.json` on the promotion branch - so `main` advances only its height, not its minor. +- **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 @@ -36,24 +40,63 @@ The repo uses a **two-phase model by default**: PRs build fast, publishing is ba - **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*. -- 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. +### Rules + +- 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 @@ -99,6 +142,80 @@ 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. 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