diff --git a/.editorconfig b/.editorconfig index f6b2fa26..de52618a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -38,6 +38,11 @@ indent_size = 2 [*.sh] end_of_line = lf +# Husky git hooks are POSIX shell scripts (extensionless) and must be LF so the +# shebang execs on Linux/WSL; otherwise [*] = crlf would break them. +[.husky/pre-commit] +end_of_line = lf + # Windows scripts [*.{cmd,bat,ps1}] end_of_line = crlf @@ -48,6 +53,47 @@ end_of_line = crlf # 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 +# 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 csharp_indent_case_contents = true diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fbc6fce9..d0822a2a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,43 +1,72 @@ # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file +# +# Every ecosystem appears **twice**: once with `target-branch: "main"` +# and once with `target-branch: "develop"`. Dependabot will open +# parallel PRs against each branch, so both stay current on +# dependency versions independently of the develop → main release +# cadence. +# +# Why dual-target and not develop-only: +# - `develop` is the integration branch and ships content forward to +# `main` through merge-commit releases, but the time between releases +# can be long (a feature branch may sit on develop for weeks). +# - Consumers (NuGet.org, GitHub releases) pull from `main` directly. +# If `main` only got dependency bumps via the next develop → main +# release, those consumers would ship outdated code in the interim. +# +# The merge-bot's `case` statement in +# .github/workflows/merge-bot-pull-request.yml dispatches the merge +# method per base ref (squash on develop, merge on main) so both bases +# auto-merge cleanly. `develop` remains strictly forward-only: there +# are no main → develop back-merges; each branch absorbs its own +# Dependabot PRs independently. +# +# Security update PRs (CVE-driven) are opened by Dependabot against +# the repo default branch (`main`) regardless of any `target-branch` +# config — the `case` statement handles them in the same code path. version: 2 updates: - # main -- package-ecosystem: "nuget" - target-branch: "main" - directory: "/" - schedule: - interval: "daily" - groups: - nuget-deps: - patterns: - - "*" -- package-ecosystem: "github-actions" - target-branch: "main" - directory: "/" - schedule: - interval: "daily" - groups: - actions-deps: - patterns: - - "*" + # ----- nuget ----- - # develop -- package-ecosystem: "nuget" - target-branch: "develop" - directory: "/" - schedule: - interval: "daily" - groups: - nuget-deps: - patterns: - - "*" -- package-ecosystem: "github-actions" - target-branch: "develop" - directory: "/" - schedule: - interval: "daily" - groups: - actions-deps: - patterns: - - "*" + - package-ecosystem: "nuget" + target-branch: "main" + directory: "/" + schedule: + interval: "daily" + groups: + nuget-deps: + patterns: + - "*" + + - package-ecosystem: "nuget" + target-branch: "develop" + directory: "/" + schedule: + interval: "daily" + groups: + nuget-deps: + patterns: + - "*" + + # ----- github-actions ----- + + - package-ecosystem: "github-actions" + target-branch: "main" + directory: "/" + schedule: + interval: "daily" + groups: + actions-deps: + patterns: + - "*" + + - package-ecosystem: "github-actions" + target-branch: "develop" + directory: "/" + schedule: + interval: "daily" + groups: + actions-deps: + patterns: + - "*" diff --git a/.github/workflows/BuildPublishPipeline.yml b/.github/workflows/BuildPublishPipeline.yml deleted file mode 100644 index f37459ee..00000000 --- a/.github/workflows/BuildPublishPipeline.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Build and publish release - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - test: - name: Test release - runs-on: ubuntu-latest - - steps: - - # https://github.com/marketplace/actions/setup-net-core-sdk - - name: Setup .NET SDK - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.x - - # https://github.com/marketplace/actions/checkout - - name: Checkout code - uses: actions/checkout@v6 - - # https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-test - - name: Run unit tests - run: dotnet test ./UtilitiesTests/UtilitiesTests.csproj - - - build: - name: Build and publish release - runs-on: ubuntu-latest - needs: test - - steps: - - # https://github.com/marketplace/actions/setup-net-core-sdk - - name: Setup .NET SDK - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.x - - # https://github.com/marketplace/actions/checkout - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - # https://github.com/marketplace/actions/nerdbank-gitversioning - - name: Run Nerdbank.GitVersioning - id: nbgv - uses: dotnet/nbgv@master - - # https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-build - - name: Build project - run: >- - dotnet build ./Utilities/Utilities.csproj - --output ./Publish/ - --configuration ${{ endsWith(github.ref, 'refs/heads/main') && 'Release' || 'Debug' }} - -property:Version=${{ steps.nbgv.outputs.AssemblyVersion }} - -property:FileVersion=${{ steps.nbgv.outputs.AssemblyFileVersion }} - -property:AssemblyVersion=${{ steps.nbgv.outputs.AssemblyVersion }} - -property:InformationalVersion=${{ steps.nbgv.outputs.AssemblyInformationalVersion }} - -property:PackageVersion=${{ steps.nbgv.outputs.SemVer2 }} - - # https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-nuget-push - - name: Publish to NuGet.org - if: ${{ github.event_name != 'pull_request' }} - run: >- - dotnet nuget push ${{ github.workspace }}/Publish/*.nupkg - --source https://api.nuget.org/v3/index.json - --api-key ${{ secrets.NUGET_API_KEY }} - --skip-duplicate - - # https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-nuget-registry - - name: Publish to GitHub NuGet Registry - if: ${{ github.event_name != 'pull_request' }} - run: >- - dotnet nuget push ${{ github.workspace }}/Publish/*.nupkg - --source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json - --api-key ${{ secrets.GITHUB_TOKEN }} - --skip-duplicate - - - name: Zip output - if: ${{ github.event_name != 'pull_request' }} - run: 7z a -t7z ./Publish/Utilities.7z ./Publish/* - - # https://github.com/marketplace/actions/gh-release - - name: Create GitHub release - if: ${{ github.event_name != 'pull_request' }} - uses: softprops/action-gh-release@v2 - with: - generate_release_notes: true - tag_name: ${{ steps.nbgv.outputs.SemVer2 }} - prerelease: ${{ !endsWith(github.ref, 'refs/heads/main') }} - files: | - LICENSE - ./Publish/Utilities.7z diff --git a/.github/workflows/DependabotAutoMerge.yml b/.github/workflows/DependabotAutoMerge.yml deleted file mode 100644 index 5cfd5a80..00000000 --- a/.github/workflows/DependabotAutoMerge.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Dependabot auto-merge - -on: - pull_request: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - dependabot: - name: Dependabot auto-merge - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - if: github.actor == 'dependabot[bot]' - - steps: - - # https://github.com/marketplace/actions/fetch-metadata-from-dependabot-prs - # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request - - name: Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@v3 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - - name: Auto-merge dependabot non-major updates - if: steps.metadata.outputs.update-type != 'version-update:semver-major' - run: gh pr merge --auto --merge "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/build-datebadge-task.yml b/.github/workflows/build-datebadge-task.yml new file mode 100644 index 00000000..7261730a --- /dev/null +++ b/.github/workflows/build-datebadge-task.yml @@ -0,0 +1,37 @@ +name: Build BYOB date badge task + +on: + workflow_call: + inputs: + # Logical branch this badge run is for. The badge only updates on + # `main`; the publisher passes the branch explicitly so a scheduled + # run building `develop` doesn't try to write the main badge. Required + # (no `github.ref_name` fallback) so the gate can't silently misfire. + branch: + required: true + type: string + +jobs: + + date-badge: + name: Build BYOB date badge job + runs-on: ubuntu-latest + + steps: + + - name: Get current date step + id: date + run: | + set -euo pipefail + echo "date=$(date)" >> "$GITHUB_OUTPUT" + + - name: Build BYOB date badge step + if: ${{ inputs.branch == 'main' }} + uses: RubbaBoy/BYOB@a4919104bc0ec7cfd7f113e42c405cc45246f2a4 # v1 + with: + name: lastbuild + label: "Last Build" + icon: "github" + status: ${{ steps.date.outputs.date }} + color: "blue" + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-nugetlibrary-task.yml b/.github/workflows/build-nugetlibrary-task.yml new file mode 100644 index 00000000..5351e188 --- /dev/null +++ b/.github/workflows/build-nugetlibrary-task.yml @@ -0,0 +1,93 @@ +name: Build NuGet library task + +env: + PROJECT_FILE: ./Utilities/Utilities.csproj + PROJECT_ARTIFACT: Utilities.7z + +on: + workflow_call: + inputs: + # Input to control whether to push the NuGet library to NuGet.org + push: + required: false + type: boolean + default: false + # Git ref to check out / version (empty = default checkout ref). + ref: + required: false + type: string + default: '' + # Logical branch driving build configuration (`main` => Release, else + # Debug). Required (no `github.ref_name` fallback, which would mislabel + # the develop leg of the publisher's matrix); the orchestrator passes it. + branch: + required: true + type: string + outputs: + # Output of the uploaded artifact id + artifact-id: + value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }} + +jobs: + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + with: + ref: ${{ inputs.ref }} + + build-nugetlibrary: + name: Build NuGet library project job + runs-on: ubuntu-latest + outputs: + artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }} + needs: [get-version] + + steps: + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + ref: ${{ inputs.ref }} + + - name: Build NuGet library project step + run: | + set -euo pipefail + dotnet build ${{ env.PROJECT_FILE }} \ + -property:OutputPath=${{ runner.temp }}/publish/ \ + -property:PackageOutputPath=${{ runner.temp }}/publish/ \ + --configuration ${{ inputs.branch == 'main' && 'Release' || 'Debug' }} \ + -property:Version=${{ needs.get-version.outputs.AssemblyVersion }} \ + -property:FileVersion=${{ needs.get-version.outputs.AssemblyFileVersion }} \ + -property:AssemblyVersion=${{ needs.get-version.outputs.AssemblyVersion }} \ + -property:InformationalVersion=${{ needs.get-version.outputs.AssemblyInformationalVersion }} \ + -property:PackageVersion=${{ needs.get-version.outputs.SemVer2 }} + + - name: Publish to NuGet.org step + if: ${{ inputs.push }} + run: | + set -euo pipefail + dotnet nuget push ${{ runner.temp }}/publish/*.nupkg \ + --source https://api.nuget.org/v3/index.json \ + --api-key ${{ secrets.NUGET_API_KEY }} \ + --skip-duplicate + + - name: Zip output step + run: | + set -euo pipefail + 7z a -t7z ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} ${{ runner.temp }}/publish/* + + # 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 + id: artifact-upload-step + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: nugetlibrary-build-${{ inputs.branch }} + path: ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml new file mode 100644 index 00000000..094e193e --- /dev/null +++ b/.github/workflows/build-release-task.yml @@ -0,0 +1,140 @@ +name: Build project release task + +on: + workflow_call: + inputs: + # Input to control whether to create a GitHub release + github: + required: false + type: boolean + default: false + # Input to control whether to push the library to NuGet.org + nuget: + required: false + type: boolean + default: false + # Git ref to check out / version (empty = default checkout ref). + ref: + required: false + type: string + default: '' + # Logical branch driving config / tags / prerelease for every target. + # Required (no `github.ref_name` fallback): the publisher builds both + # `main` and `develop` from one run whose `github.ref_name` is `main`, + # so a silent fallback would mislabel the develop leg. Every caller + # passes it explicitly; a missing value should fail loudly. + branch: + required: true + type: string + # Smoke mode: reduced, never-published build for fast PR feedback. + # Forwarded to every target; also hard-disables every push below so a + # smoke run can never publish regardless of the publish flags. + smoke: + required: false + type: boolean + default: false + # Per-target presence gate. Default true (build everything). A PR smoke + # run sets this from the paths-filter so the library only builds when it + # actually changed. + enable_nuget: + required: false + type: boolean + default: true + +jobs: + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + with: + ref: ${{ inputs.ref }} + + build-nugetlibrary: + name: Build NuGet library job + if: ${{ inputs.enable_nuget }} + needs: [get-version] + uses: ./.github/workflows/build-nugetlibrary-task.yml + secrets: inherit + with: + # Pin to the exact commit get-version resolved (immutable), not the + # possibly-moving branch ref: the publisher passes a branch name, and a + # commit landing mid-run could otherwise build artifacts from a different + # 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. + push: ${{ inputs.nuget && !inputs.smoke }} + + github-release: + name: Publish GitHub release job + # `&& !inputs.smoke` enforces the "smoke never publishes" guarantee at the + # job level too (matching the `&& !inputs.smoke` push gate above), so a + # smoke caller that also set `github: true` still can't create a release. + if: ${{ inputs.github && !inputs.smoke }} + runs-on: ubuntu-latest + needs: [get-version, build-nugetlibrary] + + steps: + + # Check out the exact built commit (NBGV `GitCommitId`), not the + # 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 + with: + ref: ${{ needs.get-version.outputs.GitCommitId }} + + - name: Download library build artifacts step + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }} + path: ./Publish + + # The weekly publisher re-runs even when a branch has no new commits, so + # 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 + # release for this tag already exists. + - name: Check for existing release step + id: release-exists + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.get-version.outputs.SemVer2 }} + run: | + set -euo pipefail + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "Release $TAG already exists; workflow_dispatch will refresh it." + else + echo "Release $TAG already exists; skipping release creation (no-op republish)." + fi + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + # `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 + # 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 + # could tag the release on a newer commit than the one that was built). + # Skip the no-op weekly republish when the tag already exists, but always + # allow a manual `workflow_dispatch` through so it can repair/refresh a + # 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 + with: + generate_release_notes: true + tag_name: ${{ needs.get-version.outputs.SemVer2 }} + target_commitish: ${{ needs.get-version.outputs.GitCommitId }} + prerelease: ${{ inputs.branch != 'main' }} + files: | + LICENSE + README.md + ./Publish/* diff --git a/.github/workflows/get-version-task.yml b/.github/workflows/get-version-task.yml new file mode 100644 index 00000000..259aa528 --- /dev/null +++ b/.github/workflows/get-version-task.yml @@ -0,0 +1,62 @@ +name: Get version information task + +on: + workflow_call: + inputs: + # Git ref to check out and version. Empty string falls back to the + # caller's default checkout ref (`github.ref`), preserving the original + # behavior. The publisher passes an explicit branch so a scheduled run — + # which always reports `github.ref` as the default branch — can still + # compute NBGV versions for `develop` too. + ref: + required: false + type: string + default: '' + outputs: + # Version information outputs + SemVer2: + value: ${{ jobs.get-version.outputs.SemVer2 }} + AssemblyVersion: + value: ${{ jobs.get-version.outputs.AssemblyVersion }} + AssemblyFileVersion: + value: ${{ jobs.get-version.outputs.AssemblyFileVersion }} + AssemblyInformationalVersion: + value: ${{ jobs.get-version.outputs.AssemblyInformationalVersion }} + # Full SHA of the commit NBGV computed the version from. Used to pin the + # GitHub release tag and the built artifacts to the exact built commit + # (immutable), rather than a moving branch ref. + GitCommitId: + value: ${{ jobs.get-version.outputs.GitCommitId }} + +jobs: + + get-version: + name: Get version information job + runs-on: ubuntu-latest + outputs: + SemVer2: ${{ steps.nbgv.outputs.SemVer2 }} + AssemblyVersion: ${{ steps.nbgv.outputs.AssemblyVersion }} + AssemblyFileVersion: ${{ steps.nbgv.outputs.AssemblyFileVersion }} + AssemblyInformationalVersion: ${{ steps.nbgv.outputs.AssemblyInformationalVersion }} + GitCommitId: ${{ steps.nbgv.outputs.GitCommitId }} + + steps: + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + ref: ${{ inputs.ref }} + fetch-depth: 0 + + # nbgv is intentionally NOT SHA-pinned: the upstream tag stream lags + # `master` substantially and Dependabot's tag-tracking would propose + # a downgrade. Documented exception to the Workflow YAML Conventions + # action-pinning rule in AGENTS.md. + - name: Run Nerdbank.GitVersioning tool step + id: nbgv + uses: dotnet/nbgv@master diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml new file mode 100644 index 00000000..ef9290da --- /dev/null +++ b/.github/workflows/merge-bot-pull-request.yml @@ -0,0 +1,149 @@ +name: Merge bot pull request action + +# Two-job model: +# 1. `merge-dependabot` runs on `opened` and `reopened` events only. It +# enables auto-merge via `gh pr merge --auto` once per PR. Restricting +# to open/reopen (skipping `synchronize`) is what makes step 2 below +# stick — if this job re-ran on every `synchronize`, it'd undo a +# maintainer-triggered disable. +# The merge method (`--squash` vs `--merge`) is dispatched by a `case` +# statement on `pull_request.base.ref` so the form matches each branch's +# ruleset (develop = squash-only, main = merge-only, see AGENTS.md +# "Branching Model"). Dependabot opens parallel PRs against both branches; +# security updates always target `main` and flow through the same path. +# 2. `disable-auto-merge-on-maintainer-push` runs on `synchronize` events +# against Dependabot-authored PRs when the event actor is NOT Dependabot +# — i.e. a maintainer pushed commits to a Dependabot PR. It calls +# `gh pr merge --disable-auto` so the maintainer's commits don't +# auto-merge along with the bot's content. The maintainer re-enables +# auto-merge manually (UI or `gh pr merge --auto`) when ready. +# +# Token strategy: +# Every job uses an App token (`actions/create-github-app-token`). The +# disable job needs an App token because, on a Dependabot PR, the workflow +# context runs with Dependabot's restricted secrets regardless of the event +# actor, so plain `GITHUB_TOKEN` would be read-only. + +# `pull_request_target` rather than `pull_request`: this workflow holds +# the App private key in env (`GH_TOKEN`) and runs actions +# (`create-github-app-token`, `fetch-metadata`) that consume it. Under +# `pull_request` the workflow definition AND the action SHAs come from +# the PR head — meaning a Dependabot bump of `actions/create-github-app-token` +# (or any other action used here) would execute the new, unreviewed SHA +# with full access to the App key. `pull_request_target` runs from the +# base-branch workflow definition, so action-SHA changes only take effect +# *after* they're merged. Safe because this workflow never checks out PR +# code — it only calls `gh pr merge` against the PR by URL, so the usual +# `pull_request_target` warning about untrusted PR code doesn't apply. +on: + pull_request_target: + types: [opened, reopened, synchronize] + +# `cancel-in-progress: false` is load-bearing. The two-job model (enable on +# opened/reopened, disable on maintainer-triggered synchronize) relies on +# those events running to completion in arrival order. With +# cancel-in-progress: true, a fast follow-up synchronize (e.g. a Dependabot +# rebase right after PR open) would cancel the in-flight `opened` run before +# it reached `gh pr merge --auto`, and the new synchronize run skips the +# enable job (opened/reopened filter), leaving auto-merge never enabled. +# Queueing instead of cancelling makes the final state deterministic: opened +# enables, then any subsequent synchronize disables (if maintainer) or no-ops +# (if bot). +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + + merge-dependabot: + name: Merge dependabot pull request job + runs-on: ubuntu-latest + # Restrict to Dependabot PRs that originate from this repository, not + # a fork. Only runs on `opened` / `reopened` events so the auto-merge + # enable happens once per PR; the `disable-auto-merge-on-maintainer-push` + # job below is what disables auto-merge when a maintainer pushes to a + # Dependabot branch. Skipping `synchronize` here is what keeps that + # disable sticky. + if: >- + (github.event.action == 'opened' || github.event.action == 'reopened') && + github.event.pull_request.user.login == 'dependabot[bot]' && + github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: write + pull-requests: write + + steps: + + - name: Generate GitHub App token step + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Get dependabot metadata step + id: metadata + uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + # Skip semver-major NuGet bumps: majors can build cleanly but break + # runtime behavior, so they should land via human review. Other + # ecosystems' majors (github-actions) are usually safe and merge. + - name: Merge pull request step + if: >- + (steps.metadata.outputs.package-ecosystem != 'nuget') || + (steps.metadata.outputs.update-type != 'version-update:semver-major') + run: | + set -euo pipefail + case "${{ github.event.pull_request.base.ref }}" in + develop) method=--squash ;; + main) method=--merge ;; + *) + echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}" + exit 1 + ;; + esac + gh pr merge --auto "$method" "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + + disable-auto-merge-on-maintainer-push: + name: Disable auto-merge on maintainer push job + runs-on: ubuntu-latest + # Fires on `synchronize` events against Dependabot-authored PRs when the + # event actor is NOT Dependabot — i.e. a maintainer pushed commits to the + # bot's branch. Disables auto-merge so the maintainer's commits don't + # auto-merge along with the bot's content. The maintainer re-enables + # auto-merge manually when ready (UI button, or `gh pr merge --auto `). + # + # `gh pr merge --disable-auto` is idempotent — calling it on a PR + # that already has auto-merge disabled is a no-op. + if: >- + github.event.action == 'synchronize' && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login == 'dependabot[bot]' && + github.actor != github.event.pull_request.user.login + permissions: + pull-requests: write + + steps: + + - name: Generate GitHub App token step + # App token rather than GITHUB_TOKEN: on a Dependabot PR the + # workflow context runs with Dependabot's restricted secrets + # regardless of who triggered the event (GitHub gates by PR + # origin, not by event actor), and the restricted GITHUB_TOKEN + # is read-only. Same App token pattern as the merge job. + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Disable auto-merge step + run: gh pr merge --disable-auto "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 00000000..0985a1ec --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,109 @@ +name: Publish project release action + +on: + push: + branches: [ main, develop ] + workflow_dispatch: + schedule: + # Weekly full build/publish of both branches on Mondays at 02:00 UTC. + # This is the guaranteed publisher in the default two-phase model: routine + # merges only smoke-test, and this scheduled run republishes everything. + - cron: '0 2 * * MON' + +# Real publishes (schedule, dispatch, or push when PUBLISH_ON_MERGE is set) +# share a single GLOBAL, ref-independent group so they serialize: a scheduled +# run and a manual dispatch both build BOTH branches regardless of the +# triggering ref, so a ref-scoped group would let a scheduled run (ref=main) +# and a manual dispatch (ref=develop) run concurrently and double-publish. +# Non-publishing `push` runs (the two-phase default) get a unique per-run group +# so they don't queue behind — or delay — a real publish; they only execute the +# no-op `setup` job and skip everything else. +concurrency: + group: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || vars.PUBLISH_ON_MERGE == 'true') && github.workflow || format('{0}-noop-{1}', github.workflow, github.run_id) }} + # Documented exception to the standard `cancel-in-progress: true` (see + # AGENTS.md "Workflow YAML Conventions"): cancelling a publish mid-flight can + # leave a half-created GitHub release or a partially pushed NuGet package. + # Queue instead of cancel so each publish runs to completion. + cancel-in-progress: false + +jobs: + + # Decide WHICH branches to publish and WHETHER to publish at all: + # - push -> publish only the pushed branch, and only when the + # `PUBLISH_ON_MERGE` repository variable is `true` + # (opt-in legacy continuous-release). Unset/false => the + # default two-phase model: merges don't publish. + # - schedule -> always publish BOTH branches (the weekly full build). + # - dispatch -> always publish BOTH branches (manual on-demand publish). + setup: + name: Resolve publish plan job + runs-on: ubuntu-latest + outputs: + branches: ${{ steps.plan.outputs.branches }} + publish: ${{ steps.plan.outputs.publish }} + steps: + - name: Compute publish plan step + id: plan + env: + # Repository variable (Settings -> Actions -> Variables). Unset reads + # as empty string, so the default is the two-phase model. + PUBLISH_ON_MERGE: ${{ vars.PUBLISH_ON_MERGE }} + run: | + set -euo pipefail + case "${{ github.event_name }}" in + push) + branches='["${{ github.ref_name }}"]' + if [[ "${PUBLISH_ON_MERGE:-}" == "true" ]]; then + publish=true + else + publish=false + fi + ;; + *) + # schedule / workflow_dispatch + branches='["main","develop"]' + publish=true + ;; + esac + echo "Event=${{ github.event_name }} branches=$branches publish=$publish" + echo "branches=$branches" >> "$GITHUB_OUTPUT" + echo "publish=$publish" >> "$GITHUB_OUTPUT" + + # Full build + publish for each planned branch. The branch matrix lets a + # single scheduled run publish both `main` (Release, non-prerelease) and + # `develop` (Debug, prerelease) — each leg checks out and versions its own + # branch via the threaded `ref`/`branch`. + publish: + name: Publish project release job + needs: [setup] + if: ${{ needs.setup.outputs.publish == 'true' }} + strategy: + fail-fast: false + matrix: + branch: ${{ fromJSON(needs.setup.outputs.branches) }} + uses: ./.github/workflows/build-release-task.yml + secrets: inherit + permissions: + contents: write + with: + ref: ${{ matrix.branch }} + branch: ${{ matrix.branch }} + smoke: false + # Push to GitHub and NuGet. + github: true + nuget: true + + date-badge: + name: Create BYOB date badge job + needs: [setup, publish] + if: ${{ needs.setup.outputs.publish == 'true' }} + strategy: + matrix: + branch: ${{ fromJSON(needs.setup.outputs.branches) }} + uses: ./.github/workflows/build-datebadge-task.yml + secrets: inherit + permissions: + contents: write + with: + # The badge task self-gates to `main`; the develop leg is a no-op. + branch: ${{ matrix.branch }} diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml new file mode 100644 index 00000000..934e986d --- /dev/null +++ b/.github/workflows/test-pull-request.yml @@ -0,0 +1,137 @@ +name: Test pull request action + +on: + pull_request: + # The `branches:` filter under `pull_request` matches the PR's BASE + # branch — only the protected base branches go here. + branches: [ main, develop ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + # Detect whether a PR actually touches the library so we only smoke-build + # when it changed. Build-workflow files are intentionally NOT in the filter: + # a path filter can't tell a logic change in a build workflow from an action- + # version bump. A workflow-only change is therefore not smoke-built — the + # reusable workflows are exercised instead by the next run that uses them (a + # later code PR's smoke build, or the scheduled/publish run); lint workflow + # edits with `actionlint` locally before pushing (there is no CI lint job). + # On `workflow_dispatch` (no PR base to diff against) the target is forced on + # so a manual run is a full smoke build. + changes: + name: Detect changed targets job + runs-on: ubuntu-latest + # `dorny/paths-filter` lists the PR's changed files via the GitHub API + # (this job does not check out the tree), which needs `pull-requests: read`. + # The repo's default GITHUB_TOKEN is restricted, so grant it explicitly. + permissions: + contents: read + pull-requests: read + outputs: + nuget: ${{ github.event_name == 'pull_request' && steps.filter.outputs.nuget || 'true' }} + steps: + - name: Filter changed paths step + id: filter + if: ${{ github.event_name == 'pull_request' }} + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + with: + filters: | + shared: &shared + - 'Directory.Build.props' + - 'Directory.Packages.props' + - 'version.json' + - '*.slnx' + nuget: + - *shared + - 'Utilities/**' + + # Unit tests are cheap and validate the library, so they always run + # regardless of whether the smoke build is gated off. + unit-test: + name: Run unit tests job + runs-on: ubuntu-latest + + steps: + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + # `dotnet husky run` is the repo's git-hook runner; it invokes the same + # CSharpier + dotnet format style checks the build conventions require. + - name: Check code style step + run: | + set -euo pipefail + dotnet tool restore + dotnet husky install + dotnet husky run + + - name: Run unit tests step + run: dotnet test + + # Fast PR feedback: build the library in smoke mode (Debug for develop / + # Release for main, no publishing). Validates the PR's base-branch + # configuration by passing `branch: github.base_ref`. Skipped entirely when + # the library didn't change (e.g. a docs-only or workflow-only PR) — unit + # tests still run. + smoke-build: + name: Smoke build changed targets job + needs: [changes, unit-test] + if: ${{ needs.changes.outputs.nuget == 'true' }} + uses: ./.github/workflows/build-release-task.yml + secrets: inherit + with: + smoke: true + # Do not publish anything from a PR. + github: false + nuget: false + # Check out the PR head by SHA (not head_ref): the head SHA is reachable + # in the base repo via refs/pull/N/head even for fork PRs, whereas the + # head_ref branch name does not exist in the base repo for forks and + # would fail checkout. Validate it in the base branch's configuration. + # `workflow_dispatch` has no pull_request payload, so fall back to the + # triggering ref. + ref: ${{ github.event.pull_request.head.sha || github.ref_name }} + branch: ${{ github.base_ref || github.ref_name }} + enable_nuget: ${{ needs.changes.outputs.nuget == 'true' }} + + # TODO: Workaround for GitHub Actions not supporting status checks on conditional jobs + # https://github.com/orgs/community/discussions/12395#discussioncomment-12970019 + # This job's name is bound to the branch ruleset as the required status check + # context — do NOT rename it (see AGENTS.md "Workflow YAML Conventions"). + check-workflow-status: + name: Check pull request workflow status + runs-on: ubuntu-latest + needs: + [ changes, unit-test, smoke-build ] + if: always() + steps: + - name: Check workflow results step + run: | + set -euo pipefail + exit_on_result() { + if [[ "$2" == "failure" || "$2" == "cancelled" ]]; then + echo "Job '$1' failed or was cancelled." + exit 1 + fi + } + # The paths-filter job MUST succeed: if it failed we don't know + # whether the library changed, so a library-changing PR could merge + # with its smoke build silently skipped. Treat anything other than + # success as a block. + if [[ "${{ needs.changes.result }}" != "success" ]]; then + echo "Job 'changes' did not succeed (${{ needs.changes.result }}); refusing to pass." + exit 1 + fi + # unit-test always runs; smoke-build may be legitimately skipped + # (library unchanged) — `skipped` passes, only failure/cancelled blocks. + exit_on_result "unit-test" "${{ needs.unit-test.result }}" + exit_on_result "smoke-build" "${{ needs.smoke-build.result }}" diff --git a/.gitignore b/.gitignore index d83e0a86..8f843cc6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ [Rr]elease/ [Bb]in/ [Oo]bj/ +.artifacts .idea .vs diff --git a/.husky/pre-commit b/.husky/pre-commit index 5cc3c736..818853f5 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -dotnet husky run +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +dotnet husky run diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..489e7474 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,61 @@ +# 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). + +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. +- **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. +- **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. +- **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 }}`. + +## Release Model + +The repo uses a **two-phase model by default**: PRs build fast, publishing is batched. + +- **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. +- **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. + +## 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. + +## 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: >-`. +- **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. + +## Pull Request Title and Commit Message Conventions + +- Imperative subject summarizing the change, ≤72 characters, no trailing period. +- Don't write vague titles (`update stuff`, `wip`). Dependabot's default `Bump X from Y to Z` titles are fine. +- Don't add `Co-Authored-By:` lines unless the developer explicitly asks. +- Use US English spelling. + +## 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. +- **Rulesets**: `develop` squash-only, `main` merge-only; both require the `Check pull request workflow status` check and signed commits; both omit "Require branches to be up to date before merging". diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..da26fe38 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,14 @@ + + + net10.0 + enable + enable + latest-all + All + true + true + $(MSBuildThisFileDirectory).artifacts + false + true + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..ded83a3c --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/README.md b/README.md index a2157fb7..fae204a0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Some useful and not so useful C# .NET utility classes. Code and Pipeline is on [GitHub](https://github.com/ptr727/Utilities)\ ![GitHub Last Commit](https://img.shields.io/github/last-commit/ptr727/Utilities?logo=github)\ -![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ptr727/Utilities/BuildPublishPipeline.yml?logo=github) +![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ptr727/Utilities/publish-release.yml?logo=github) ## NuGet Package diff --git a/Sandbox/Program.cs b/Sandbox/Program.cs index 9e8db568..a18b2f6f 100644 --- a/Sandbox/Program.cs +++ b/Sandbox/Program.cs @@ -1,12 +1,11 @@ using System.Diagnostics; -using System.IO; using System.Reflection; using Serilog; // Get the assembly directory Assembly? entryAssembly = Assembly.GetEntryAssembly(); Debug.Assert(entryAssembly != null); -string? assemblyDirectory = Path.GetDirectoryName(System.AppContext.BaseDirectory); +string? assemblyDirectory = Path.GetDirectoryName(AppContext.BaseDirectory); Debug.Assert(assemblyDirectory != null); string projectDirectory = Path.GetFullPath(Path.Combine(assemblyDirectory, "../../../../")); Log.Logger.Information("Project directory: {ProjectDirectory}", projectDirectory); diff --git a/Sandbox/Sandbox.csproj b/Sandbox/Sandbox.csproj index dffe5e7e..376f9485 100644 --- a/Sandbox/Sandbox.csproj +++ b/Sandbox/Sandbox.csproj @@ -1,17 +1,16 @@ Exe - net10.0 true false - latest - true true - enable + + $(NoWarn);IL3058 - - + + diff --git a/Utilities.slnx b/Utilities.slnx index 289d0532..598b2df6 100644 --- a/Utilities.slnx +++ b/Utilities.slnx @@ -4,6 +4,9 @@ + + + @@ -15,7 +18,13 @@ - + + + + + + + diff --git a/Utilities/CommandLineEx.cs b/Utilities/CommandLineEx.cs index 22af4983..5a2fd1d6 100644 --- a/Utilities/CommandLineEx.cs +++ b/Utilities/CommandLineEx.cs @@ -1,5 +1,3 @@ -using System; - namespace InsaneGenius.Utilities; /// diff --git a/Utilities/ConsoleEx.cs b/Utilities/ConsoleEx.cs index 4947dd4e..14712599 100644 --- a/Utilities/ConsoleEx.cs +++ b/Utilities/ConsoleEx.cs @@ -1,6 +1,4 @@ -using System; using System.Globalization; -using System.Threading; namespace InsaneGenius.Utilities; diff --git a/Utilities/Download.cs b/Utilities/Download.cs index 39875539..9489733d 100644 --- a/Utilities/Download.cs +++ b/Utilities/Download.cs @@ -1,10 +1,5 @@ -using System; -using System.IO; -using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; -using System.Threading; -using System.Threading.Tasks; namespace InsaneGenius.Utilities; diff --git a/Utilities/Extensions.cs b/Utilities/Extensions.cs index 43641238..2002e526 100644 --- a/Utilities/Extensions.cs +++ b/Utilities/Extensions.cs @@ -1,8 +1,5 @@ -using System; using System.IO.Compression; using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; using Serilog; namespace InsaneGenius.Utilities; diff --git a/Utilities/FileEx.cs b/Utilities/FileEx.cs index 0c2a9607..a2c02799 100644 --- a/Utilities/FileEx.cs +++ b/Utilities/FileEx.cs @@ -1,12 +1,6 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; namespace InsaneGenius.Utilities; diff --git a/Utilities/FileExOptions.cs b/Utilities/FileExOptions.cs index 5e41ae57..fb154924 100644 --- a/Utilities/FileExOptions.cs +++ b/Utilities/FileExOptions.cs @@ -1,5 +1,3 @@ -using System.Threading; - namespace InsaneGenius.Utilities; /// diff --git a/Utilities/Format.cs b/Utilities/Format.cs index 412fa140..91675fd8 100644 --- a/Utilities/Format.cs +++ b/Utilities/Format.cs @@ -1,5 +1,3 @@ -using System; - namespace InsaneGenius.Utilities; /// diff --git a/Utilities/StringCompression.cs b/Utilities/StringCompression.cs index a9f0ef4f..fee7f796 100644 --- a/Utilities/StringCompression.cs +++ b/Utilities/StringCompression.cs @@ -1,9 +1,5 @@ -using System; -using System.IO; using System.IO.Compression; using System.Text; -using System.Threading; -using System.Threading.Tasks; namespace InsaneGenius.Utilities; diff --git a/Utilities/StringHistory.cs b/Utilities/StringHistory.cs index cf62c870..2b238b83 100644 --- a/Utilities/StringHistory.cs +++ b/Utilities/StringHistory.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; - namespace InsaneGenius.Utilities; /// diff --git a/Utilities/Utilities.csproj b/Utilities/Utilities.csproj index b602c118..5875b263 100644 --- a/Utilities/Utilities.csproj +++ b/Utilities/Utilities.csproj @@ -1,12 +1,13 @@ - net10.0 true false - latest - true true - enable + + $(NoWarn);IL3058 + true true true Pieter Viljoen @@ -29,8 +30,8 @@ snupkg - - + + diff --git a/UtilitiesTests/ConsoleTests.cs b/UtilitiesTests/ConsoleTests.cs index 60c91871..572073c5 100644 --- a/UtilitiesTests/ConsoleTests.cs +++ b/UtilitiesTests/ConsoleTests.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using Xunit; namespace InsaneGenius.Utilities.Tests; diff --git a/UtilitiesTests/DownloadAsyncTests.cs b/UtilitiesTests/DownloadAsyncTests.cs index f8ece729..6336ceca 100644 --- a/UtilitiesTests/DownloadAsyncTests.cs +++ b/UtilitiesTests/DownloadAsyncTests.cs @@ -1,7 +1,3 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; using Xunit; namespace InsaneGenius.Utilities.Tests; diff --git a/UtilitiesTests/DownloadTests.cs b/UtilitiesTests/DownloadTests.cs index 377b37e8..748c8647 100644 --- a/UtilitiesTests/DownloadTests.cs +++ b/UtilitiesTests/DownloadTests.cs @@ -1,4 +1,3 @@ -using System; using Xunit; namespace InsaneGenius.Utilities.Tests; diff --git a/UtilitiesTests/ExtensionsTests.cs b/UtilitiesTests/ExtensionsTests.cs index 116ecc3a..e272aa33 100644 --- a/UtilitiesTests/ExtensionsTests.cs +++ b/UtilitiesTests/ExtensionsTests.cs @@ -1,7 +1,4 @@ -using System; using System.IO.Compression; -using System.Threading; -using System.Threading.Tasks; using Serilog; using Serilog.Core; using Xunit; @@ -91,7 +88,7 @@ public void StringExtension_Compress_WithNullString_ShouldThrow() { string? nullString = null; - _ = Assert.Throws(() => nullString!.Compress()); + _ = Assert.Throws(() => nullString.Compress()); } #endregion @@ -204,7 +201,7 @@ public async Task StringExtension_CompressAsync_WithNullString_ShouldThrow() string? nullString = null; _ = await Assert.ThrowsAsync(async () => - await nullString!.CompressAsync() + await nullString.CompressAsync() ); } @@ -214,7 +211,7 @@ public async Task StringExtension_DecompressAsync_WithNullString_ShouldThrow() string? nullString = null; _ = await Assert.ThrowsAsync(async () => - await nullString!.DecompressAsync() + await nullString.DecompressAsync() ); } diff --git a/UtilitiesTests/FileExAsyncTests.cs b/UtilitiesTests/FileExAsyncTests.cs index a6fb6639..92c38600 100644 --- a/UtilitiesTests/FileExAsyncTests.cs +++ b/UtilitiesTests/FileExAsyncTests.cs @@ -1,7 +1,3 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; using Xunit; namespace InsaneGenius.Utilities.Tests; diff --git a/UtilitiesTests/StringCompressionAsyncTests.cs b/UtilitiesTests/StringCompressionAsyncTests.cs index a7a0f0e8..39fb5ae9 100644 --- a/UtilitiesTests/StringCompressionAsyncTests.cs +++ b/UtilitiesTests/StringCompressionAsyncTests.cs @@ -1,7 +1,4 @@ -using System; using System.IO.Compression; -using System.Threading; -using System.Threading.Tasks; using Xunit; namespace InsaneGenius.Utilities.Tests; diff --git a/UtilitiesTests/StringCompressionTests.cs b/UtilitiesTests/StringCompressionTests.cs index 20d44cc7..29b5c827 100644 --- a/UtilitiesTests/StringCompressionTests.cs +++ b/UtilitiesTests/StringCompressionTests.cs @@ -1,4 +1,3 @@ -using System; using Xunit; namespace InsaneGenius.Utilities.Tests; diff --git a/UtilitiesTests/StringHistoryTests.cs b/UtilitiesTests/StringHistoryTests.cs index cf07d639..c7df5ae8 100644 --- a/UtilitiesTests/StringHistoryTests.cs +++ b/UtilitiesTests/StringHistoryTests.cs @@ -1,4 +1,3 @@ -using System; using Xunit; namespace InsaneGenius.Utilities.Tests; diff --git a/UtilitiesTests/UtilitiesTests.cs b/UtilitiesTests/UtilitiesTests.cs index 6e283c91..4c9fcf65 100644 --- a/UtilitiesTests/UtilitiesTests.cs +++ b/UtilitiesTests/UtilitiesTests.cs @@ -1,5 +1,3 @@ -using System; - namespace InsaneGenius.Utilities.Tests; public class UtilitiesTests : IDisposable diff --git a/UtilitiesTests/UtilitiesTests.csproj b/UtilitiesTests/UtilitiesTests.csproj index 9ec93aea..034db127 100644 --- a/UtilitiesTests/UtilitiesTests.csproj +++ b/UtilitiesTests/UtilitiesTests.csproj @@ -1,9 +1,5 @@ - net10.0 - latest - true - enable false Pieter Viljoen Pieter Viljoen @@ -23,15 +19,15 @@ snupkg - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - +