From e299ee94553f34dd41c70ebc5fcd0e24df493bfb Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Tue, 29 Sep 2026 13:28:47 +0100 Subject: [PATCH] docs(release): release v1 --- .../prompts/sdk-diagnose-agent-folder-copy.md | 21 ++++-- .github/release.yml | 30 ++++++++ CHANGELOG.md | 72 ++++++++++++++++++ Directory.Packages.props | 2 +- README.md | 8 +- docs/wiki/Contributing.md | 13 ++++ docs/wiki/Home.md | 2 + docs/wiki/Release-Flow.md | 39 ++++++++++ docs/wiki/Release-Notes.md | 43 +++++++++++ docs/wiki/Versioning.md | 50 +++++++++++++ docs/wiki/_Sidebar.md | 4 +- global.json | 2 +- package.json | 2 +- src/src/ResourceKit/Sdk/README.md | 2 + .../AnalyzerReleases.Shipped.md | 6 +- .../AnalyzerReleases.Unshipped.md | 4 - .../AnalyzerReleaseTrackingTests.cs | 75 +++++++++++++++++++ .../SourceGeneration.UnitTests.csproj | 14 ++++ 18 files changed, 374 insertions(+), 15 deletions(-) create mode 100644 .github/release.yml create mode 100644 CHANGELOG.md create mode 100644 docs/wiki/Release-Notes.md create mode 100644 docs/wiki/Versioning.md create mode 100644 src/tests/SourceGeneration.UnitTests/AnalyzerReleaseTrackingTests.cs diff --git a/.agents/prompts/sdk-diagnose-agent-folder-copy.md b/.agents/prompts/sdk-diagnose-agent-folder-copy.md index 85ad444..e234df9 100644 --- a/.agents/prompts/sdk-diagnose-agent-folder-copy.md +++ b/.agents/prompts/sdk-diagnose-agent-folder-copy.md @@ -11,16 +11,27 @@ destination in a consuming repository. 3. Check `EnableAgentFolderInPackage` is not set to `false` anywhere in the build (project file, `Directory.Build.props`, or command-line `-p:` overrides). 4. Confirm the destination folder: default is `.agents` at the repo root, overridable per-build with - `-p:AgentPackDestinationFolder=`. + `-p:AgentPackDestinationFolder=`, and the source defaults to the package-level `.agents` + folder (override with `PurviewAgentFolderSourcePath`). 5. Verify repo-root discovery succeeded: explicit `RepoRoot`, then a nearby `AGENTS.md`, then source-control root metadata. -6. Re-run the build and confirm the destination folder now contains the copied files (including the +6. If the destination looks stale or incomplete, inspect the change-detection manifest + (`/.purview/agent-sync.cache`). It lists the files the SDK believes it already mirrored; a + matching entry with a present destination file means the sync was skipped as up to date. Delete the + manifest (or the affected destination file) to force a fresh copy. +7. Retry notices are demoted to low-importance messages, so a healthy build shows no `MSB3026` warnings. + A copy that still fails after every retry is reported as an error naming the source, destination and + OS error - search the build log for `The Purview SDK could not copy`. Temporarily set + `PurviewAgentFolderCopyRetries` / `PurviewAgentFolderCopyRetryDelayMilliseconds` to retry longer, and + `PurviewSuppressCopyRetryWarnings=false` to see every retry attempt. +8. Re-run the build and confirm the destination folder now contains the copied files (including the generated `.gitignore` for skill/prompt/agent subfolders). ## Suggested output -- A short root-cause explanation (missing import, disabled flag, wrong destination override, or repo-root - discovery miss). +- A short root-cause explanation (missing import, disabled flag, wrong destination override, repo-root + discovery miss, or a destination held open by another process). - The exact command used to reproduce/verify the fix (for example `dotnet build -p:AgentPackDestinationFolder=`). -- Confirmation that the expected files exist at the resolved destination path. +- Confirmation that the expected files exist at the resolved destination path, plus whether the + manifest skipped the sync (in which case the content was already up to date). diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..22a93fd --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,30 @@ +# Configuration for GitHub's automatically generated release notes. +# The shared Purview release pipeline creates the GitHub release and uses these +# categories to group merged pull requests. +changelog: + exclude: + labels: + - ignore-for-release + - skip-changelog + categories: + - title: Features + labels: + - feature + - enhancement + - title: Fixes + labels: + - bug + - fix + - title: Documentation + labels: + - docs + - documentation + - title: Build and tooling + labels: + - build + - ci + - dependencies + - chore + - title: Other Changes + labels: + - "*" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3aa3e9a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,72 @@ +# Changelog + +All notable changes to `Purview.Aspire.ResourceKit` are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project +adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-09-29 + +The first stable release of `Purview.Aspire.ResourceKit`. + +### Added + +- **Host kits** — `[HostKit]` marks the single host kit per compilation and generates the host base + class, per-resource properties, typed host and resource options, and an AppHost extension method + (`Add()`, for example `AddAspireResourceKit()`). +- **Resource kits** — `[ResourceDefinition]` and `[ResourceDefinition]` mark resource kit + classes; the generator emits the `sealed partial` skeleton, the `ResourceKitBase` base, + and the constructor/options wiring. +- **Predictable lifecycle** — `Build`/`BuildResource(...)` constructs each resource and + `Configure`/`ConfigureResource()` wires resources together afterwards. +- **Enablement** — `IsEnabled` (persisted toggle, usually from generated options) and + `IsResourceEnabled(builder)` (runtime hook) decide participation before resource construction. +- **Typed options** — generated `sealed partial` host and resource option types that bind from + configuration, can be extended with partial classes, and are registered with `ValidateOnStart()`. +- **`OptionsHelper`** — strongly typed assignment expressions plus binder-style flattening + (`Assign`, `AsEnvironmentVariables`, `Environment`, `Override`, `Ignore`, `PathFor`, + `SectionNameFor`) for tests and scenario toggles. +- **Project resources** — first-class support for `AddProject()`, including Aspire-generated + `Projects.*` metadata types, with dedicated generator and analyzer validation. +- **Diagnostics SG0001–SG0020** — model validation reported by the bundled analyzer, an IDE code fix + ("Split into separate assignments") for SG0020, and automatic `CS8618` suppression for + non-nullable `IResourceBuilder` properties. +- **Bundled agent skill** — `aspire-apphost-to-resourcekit`, auto-installed into consuming + repositories under `.agents/skills/**` on build (opt out with + `false`). + +### Changed + +- Host wiring is generated from attributes by default; the manual `HostKitBase` / + `ResourceKitBase` composition pattern remains fully supported for teams that + want explicit control. + +### Diagnostics + +| ID | Severity | Description | +| --- | --- | --- | +| SG0001 | Error | Class must be `partial` | +| SG0002 | Info | No resources defined for the host kit | +| SG0003 | Warning | No host kit defined (but resources exist) | +| SG0004 | Error | Multiple `[HostKit]` classes defined | +| SG0005 | Error | Duplicate resource property name | +| SG0006 | Error | Resource must derive from the expected generated base | +| SG0007 | Error | Resource name could not be derived and no `Name` was specified | +| SG0008 | Error | Explicit `PropertyName` is not a valid C# identifier | +| SG0009 | Error | Missing `IServiceCollection` type dependency | +| SG0010 | Error | Missing configuration binder dependency | +| SG0011 | Error | Missing options configuration extensions dependency | +| SG0012 | Error | Non-empty constructors are not supported on attributed classes | +| SG0013 | Error | Mixed `ResourceDefinition` attribute usage on one class | +| SG0014 | Error | Non-generic `ResourceDefinition` requires an explicit compatible base | +| SG0015 | Error | Generic `ResourceDefinition` must not declare an explicit base | +| SG0016 | Error | No Aspire resource type could be inferred/found | +| SG0017 | Warning | `IResourceBuilder` property is never assigned (execution-only) | +| SG0018 | Warning | Project resource kit does not add the declared project via `AddProject()` (execution-only) | +| SG0019 | Warning | Project resource kit explicit base does not use `ProjectResource` (execution-only) | +| SG0020 | Error | `OptionsHelper.Assign` action sets more than one property path | + +SG0001–SG0016 block generation; SG0017–SG0019 are execution-only warnings that never block +generation, so a resource kit with incomplete wiring is still generated. + +[1.0.0]: https://github.com/purview-dev/aspire-resourcekit/releases/tag/v1.0.0 diff --git a/Directory.Packages.props b/Directory.Packages.props index b03c618..062f2cb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ 5.9.0 1.68.17 1.0.0-prerelease.54 - 5.0.0-prerelease.19 + 5.0.0 10.0.12 10.10.0 1.19.0 diff --git a/README.md b/README.md index 869b378..e0655d5 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,9 @@ If your AppHost is getting bigger, this helps you keep resource setup maintainable and discoverable by moving composition into focused resource classes and generating the plumbing for you. +> [!NOTE] +> `Purview.Aspire.ResourceKit` 1.0.0 is the first stable release. See the [changelog](CHANGELOG.md) and [release notes](docs/wiki/Release-Notes.md). + > [!TIP] > **Lifecycle quick links** > @@ -62,7 +65,7 @@ The extension method name is generated from your host metadata; in this reposito > > `false` -## Lifecycle mental model (early cheat sheet) +## Lifecycle mental model ResourceKit has two layers of lifecycle methods: @@ -94,6 +97,9 @@ From your `[HostKit]` and `[ResourceDefinition]` declarations, ResourceKit gener - [Configuration and options](docs/wiki/Configuration-and-Options.md) - [Diagnostics and troubleshooting](docs/wiki/Diagnostics.md) - [Examples: generated vs manual](docs/wiki/Examples.md) +- [Release notes](docs/wiki/Release-Notes.md) +- [Versioning and compatibility](docs/wiki/Versioning.md) +- [Changelog](CHANGELOG.md) - NuGet package deep dive (API and generator behavior): [`src/src/ResourceKit/Sdk/README.md`](src/src/ResourceKit/Sdk/README.md) - Workspace notes for contributors: [`src/README.md`](src/README.md) diff --git a/docs/wiki/Contributing.md b/docs/wiki/Contributing.md index 592ad41..a8d0573 100644 --- a/docs/wiki/Contributing.md +++ b/docs/wiki/Contributing.md @@ -58,6 +58,19 @@ The repository carries agent skills and prompt workflows under `.agents/`. Consu on source generators, tests, the project SDK, or conventional commits. AGENTS.md is the canonical agent guidance and references `.agents/` for reusable workflows. +## Releases + +Releases are produced by the shared pipeline when a version bump is merged to `main`; see +[Release Flow](Release-Flow.md). When preparing a release: + +1. Update `package.json` (the authoritative version) and align `global.json` and + `Directory.Packages.props` with stable dependency versions. +2. Move new diagnostics from `src/src/SourceGeneration/AnalyzerReleases.Unshipped.md` into + `src/src/SourceGeneration/AnalyzerReleases.Shipped.md` under the released version, leaving the + unshipped file empty. +3. Add a [`CHANGELOG.md`](../../CHANGELOG.md) entry and update the wiki [release notes](Release-Notes.md). +4. Validate with `just lint-check` and `just pipeline-pack-validate` before merging. + ## Pull requests Pull requests target `main` and are validated by `.github/workflows/pr.yml`, which delegates to the diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index 65f6971..022bd25 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -24,6 +24,8 @@ moving composition into focused resource classes and generating the plumbing for - [Source Generator Behaviors](Source-Generator-Behaviors.md) - [Contributing](Contributing.md) - [Release Flow](Release-Flow.md) +- [Release Notes](Release-Notes.md) +- [Versioning](Versioning.md) ## Why teams use it diff --git a/docs/wiki/Release-Flow.md b/docs/wiki/Release-Flow.md index 1b621a3..cdfc537 100644 --- a/docs/wiki/Release-Flow.md +++ b/docs/wiki/Release-Flow.md @@ -63,6 +63,45 @@ Prerelease versions (any SemVer containing a hyphen, for example `1.0.0-prerelea through the same push-to-`main` flow. The `v` tag and GitHub release are still created and packages published; the shared pipeline does not mark the GitHub release with the prerelease flag. +## Stable release preparation + +A stable release (for example `1.0.0`) follows the same push-to-`main` flow, but requires version +preparation before the merge: + +1. Set `package.json` `version` to the stable version (for example `1.0.0`). +2. Align the build SDK in `global.json` and the centrally-managed package versions in + `Directory.Packages.props` with their stable releases. +3. Move every entry from `src/src/SourceGeneration/AnalyzerReleases.Unshipped.md` into + `src/src/SourceGeneration/AnalyzerReleases.Shipped.md` under the released version, and leave the + unshipped file empty (see below). +4. Add the release to `CHANGELOG.md` and update the [release notes](Release-Notes.md) page. +5. Validate locally (`just lint-check` and `just pipeline-pack-validate`) before merging to `main`. + +The GitHub release is created with automatically generated notes, grouped by the categories in +`.github/release.yml`. + +## Analyzer release tracking + +`src/src/SourceGeneration` ships public diagnostics (`SG0001`–`SG0020`), so it maintains the Roslyn +release tracking files: + +- `AnalyzerReleases.Shipped.md` — rules that have shipped in a released version. +- `AnalyzerReleases.Unshipped.md` — rules added or changed since the last release. + +`Microsoft.CodeAnalysis.Analyzers` automatically adds these files as analyzer additional files when +they exist in the project directory, so they are validated during the build (Roslyn components build +with `TreatWarningsAsErrors`). Add new rules to the *unshipped* file while developing, and move them +into the *shipped* file as part of a release. See [Contributing](Contributing.md) for the day-to-day +workflow. + +## Build-time dependencies + +Some centrally-managed packages are build/analysis-time only (consumed with `PrivateAssets`), for +example `Purview.Telemetry.SourceGenerator`, which `Purview.BuildSdk` injects into every non-test C# +project, and `Purview.SourceGeneratorFramework`. They never flow to consumers of the NuGet package, +but the repository cannot restore unless the pinned version exists on a configured feed. A stable +release therefore requires the pinned versions to be resolvable before merging to `main`. + ## NuGet publishing NuGet publishing uses the shared workflow's API-key path with the organization `NUGET__APIKEY` secret diff --git a/docs/wiki/Release-Notes.md b/docs/wiki/Release-Notes.md new file mode 100644 index 0000000..48b628b --- /dev/null +++ b/docs/wiki/Release-Notes.md @@ -0,0 +1,43 @@ +# Release notes + +Stable release history for `Purview.Aspire.ResourceKit`, with the highlights that matter when you +upgrade from the `1.0.0-prerelease.*` line. The full, machine-readable history lives in +[`CHANGELOG.md`](https://github.com/purview-dev/aspire-resourcekit/blob/main/CHANGELOG.md) and on +the [GitHub releases page](https://github.com/purview-dev/aspire-resourcekit/releases). + +## 1.0.0 + +The first stable release. The public surface that shipped during the prerelease line is now frozen +under [semantic versioning](Versioning.md). + +### Highlights + +- **Attributes drive generation** — `[HostKit]` and `[ResourceDefinition]` / + `[ResourceDefinition]` produce the host kit, resource kit skeletons, typed options, and + the AppHost extension method. See [Attributes Reference](Attributes-Reference.md) and + [Generated Output](Generated-Output.md). +- **Deterministic lifecycle** — `Build`/`BuildResource(...)` constructs resources, then + `Configure`/`ConfigureResource()` wires them together. See + [Lifecycle: Build vs Configure](Lifecycle-Build-Configure.md). +- **Declarative enablement** — `IsEnabled` and `IsResourceEnabled(builder)`. See + [Enablement](Enablement.md). +- **Typed, extendable options** — generated `sealed partial` options bound from configuration. See + [Configuration and Options](Configuration-and-Options.md). +- **Test-friendly overrides** — `OptionsHelper` for args, environment variables, and flattening. See + [OptionsHelper](OptionsHelper.md) and [Testing with ResourceKit](Testing-with-ResourceKit.md). +- **Project resources** — first-class `AddProject()` support with SG0018/SG0019 validation. See + [Project Resources](Project-Resources.md). +- **Diagnostics SG0001–SG0020** with an IDE code fix for SG0020. See [Diagnostics](Diagnostics.md). +- **Bundled agent skill** — `aspire-apphost-to-resourcekit` auto-installs into `.agents/skills/**`. + See [Bundled Agent Skills](Agent-Skills.md). + +### Upgrade notes + +- No public API changes are required when moving from `1.0.0-prerelease.*` to `1.0.0`. +- Pin `Purview.Aspire.ResourceKit` to `1.0.0` (or a later compatible `1.x`). + +## See also + +- [Versioning and compatibility](Versioning.md) +- [Release flow](Release-Flow.md) +- [Getting Started](Getting-Started.md) diff --git a/docs/wiki/Versioning.md b/docs/wiki/Versioning.md new file mode 100644 index 0000000..6554d09 --- /dev/null +++ b/docs/wiki/Versioning.md @@ -0,0 +1,50 @@ +# Versioning and compatibility + +`Purview.Aspire.ResourceKit` follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) +(`MAJOR.MINOR.PATCH`). The release version is authoritative in `package.json`; the release pipeline +tags the commit `v` and publishes matching NuGet packages. + +## What the version numbers mean + +- **MAJOR** — a breaking change to the public surface (see below). +- **MINOR** — additive, backwards-compatible functionality. +- **PATCH** — backwards-compatible fixes and documentation updates. + +## Prereleases + +Prereleases use a hyphenated suffix (for example `1.0.0-prerelease.41`) and ship through the same +push-to-`main` pipeline. They are published so consumers can validate upcoming changes early; they +carry no stability guarantee. `1.0.0` is the first stable release. + +## What counts as a breaking change + +Once a version is stable, the following are treated as public contracts: + +- The attributes `[HostKit]`, `[ResourceDefinition]`, and `[ResourceDefinition]`, + including their named arguments (`Name`, `PropertyName`, `ExtensionMethodName`, `GenerateOptions`). +- The runtime types and members consumed by AppHost code (`HostKitBase`, `ResourceKitBase`, + `OptionsHelper`, and the `IResourceBuilder`/`WithEnvironment` extensions). +- The names and shapes of generated members (`BuildResource`, `ConfigureResource`, + `IsResourceEnabled`, generated host properties, generated options types, and the generated + `Add()` extension method). +- The diagnostic IDs `SG0001`–`SG0020`, their meanings, and their severities. + +Renaming or removing any of these requires a MAJOR version and a documented migration path. + +### Not covered + +- The exact text of generated code and diagnostic messages. +- Files generated into a consuming repository (for example the copied `.agents/skills/**` folder). +- Build-time-only dependencies consumed through `PrivateAssets` (they never flow to consumers). + +## Diagnostics as contracts + +Diagnostic IDs are stable identifiers. New rules are introduced in the *unshipped* analyzer release +tracking file and only become part of a released version when they move into the *shipped* file as +part of a release. See [Contributing](Contributing.md) and [Release flow](Release-Flow.md) for the +release-time procedure. + +## See also + +- [Release notes](Release-Notes.md) +- [Release flow](Release-Flow.md) diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index ba36c1c..af72751 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -13,4 +13,6 @@ - [Bundled Agent Skills](Agent-Skills.md) - [Source Generator Behaviors](Source-Generator-Behaviors.md) - [Contributing](Contributing.md) -- [Release Flow](Release-Flow.md) \ No newline at end of file +- [Release Flow](Release-Flow.md) +- [Release Notes](Release-Notes.md) +- [Versioning](Versioning.md) diff --git a/global.json b/global.json index 0961f09..7f367b1 100644 --- a/global.json +++ b/global.json @@ -3,7 +3,7 @@ "allowPrerelease": false }, "msbuild-sdks": { - "Purview.BuildSdk": "1.0.0-prerelease.60" + "Purview.BuildSdk": "1.0.0" }, "test": { "runner": "Microsoft.Testing.Platform" diff --git a/package.json b/package.json index 3ff06c6..4e956cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "purview-aspire-resourcekit", - "version": "1.0.0-prerelease.41", + "version": "1.0.0", "keywords": [], "homepage": "https://purview.dev/projects/aspire-resourcekit/", "bugs": { diff --git a/src/src/ResourceKit/Sdk/README.md b/src/src/ResourceKit/Sdk/README.md index ad86c84..af18698 100644 --- a/src/src/ResourceKit/Sdk/README.md +++ b/src/src/ResourceKit/Sdk/README.md @@ -251,3 +251,5 @@ For [troubleshooting guidance](https://purview.dev/docs/aspire-resourcekit/diagn - [Homepage](https://purview.dev/projects/aspire-resourcekit/) - [Documentation](https://purview.dev/docs/aspire-resourcekit/) +- [Changelog](https://github.com/purview-dev/aspire-resourcekit/blob/main/CHANGELOG.md) +- [Release notes](https://github.com/purview-dev/aspire-resourcekit/releases) diff --git a/src/src/SourceGeneration/AnalyzerReleases.Shipped.md b/src/src/SourceGeneration/AnalyzerReleases.Shipped.md index 4b91cdf..c0de7ab 100644 --- a/src/src/SourceGeneration/AnalyzerReleases.Shipped.md +++ b/src/src/SourceGeneration/AnalyzerReleases.Shipped.md @@ -19,4 +19,8 @@ SG0013 | Purview.Aspire.ResourceKit.SourceGenerator | Error | Mixed ResourceDefinition attribute usage is not supported SG0014 | Purview.Aspire.ResourceKit.SourceGenerator | Error | Non-generic ResourceDefinition requires explicit base SG0015 | Purview.Aspire.ResourceKit.SourceGenerator | Error | Generic ResourceDefinition cannot have explicit base - SG0016 | Purview.Aspire.ResourceKit.SourceGenerator | Error | No Aspire resource found \ No newline at end of file + SG0016 | Purview.Aspire.ResourceKit.SourceGenerator | Error | No Aspire resource found + SG0017 | Purview.Aspire.ResourceKit.SourceGenerator | Warning | Resource property is never set + SG0018 | Purview.Aspire.ResourceKit.SourceGenerator | Warning | Project resource definition mismatch + SG0019 | Purview.Aspire.ResourceKit.SourceGenerator | Warning | Project resource kit base must use ProjectResource + SG0020 | Purview.Aspire.ResourceKit.SourceGenerator | Error | OptionsHelper.Assign action must set exactly one property path diff --git a/src/src/SourceGeneration/AnalyzerReleases.Unshipped.md b/src/src/SourceGeneration/AnalyzerReleases.Unshipped.md index 5f5b5f5..e9b1a80 100644 --- a/src/src/SourceGeneration/AnalyzerReleases.Unshipped.md +++ b/src/src/SourceGeneration/AnalyzerReleases.Unshipped.md @@ -2,7 +2,3 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------ -SG0017 | Purview.Aspire.ResourceKit.SourceGenerator | Warning | Resource property is never set -SG0018 | Purview.Aspire.ResourceKit.SourceGenerator | Warning | Project resource definition mismatch -SG0019 | Purview.Aspire.ResourceKit.SourceGenerator | Warning | Project resource kit base must use ProjectResource -SG0020 | Purview.Aspire.ResourceKit.SourceGenerator | Error | OptionsHelper.Assign action must set exactly one property path diff --git a/src/tests/SourceGeneration.UnitTests/AnalyzerReleaseTrackingTests.cs b/src/tests/SourceGeneration.UnitTests/AnalyzerReleaseTrackingTests.cs new file mode 100644 index 0000000..5a3f238 --- /dev/null +++ b/src/tests/SourceGeneration.UnitTests/AnalyzerReleaseTrackingTests.cs @@ -0,0 +1,75 @@ +using System.Reflection; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Purview.Aspire.ResourceKit.SourceGeneration.Helpers; + +namespace Purview.Aspire.ResourceKit.SourceGeneration; + +/// +/// Guards the Roslyn analyzer release tracking files. Every diagnostic descriptor defined by +/// must be listed in exactly one of +/// AnalyzerReleases.Shipped.md or AnalyzerReleases.Unshipped.md, so a rule can never be +/// introduced without a release tracking entry. +/// +public sealed partial class AnalyzerReleaseTrackingTests +{ + const string ReleaseTrackingFolder = "AnalyzerReleases"; + + [GeneratedRegex(@"^\s*(?SG\d{4})\s*\|", RegexOptions.CultureInvariant)] + private static partial Regex RuleIdPattern(); + + [Test] + public async Task ReleaseTrackingFiles_EveryDiagnosticDescriptor_IsTrackedExactlyOnce( + CancellationToken cancellationToken + ) + { + // Arrange + var descriptorIds = GetDescriptorIds(); + var shippedIds = await ReadRuleIdsAsync("AnalyzerReleases.Shipped.md", cancellationToken); + var unshippedIds = await ReadRuleIdsAsync("AnalyzerReleases.Unshipped.md", cancellationToken); + + // Act + var untracked = descriptorIds.Where(id => !shippedIds.Contains(id) && !unshippedIds.Contains(id)).ToArray(); + var trackedInBoth = shippedIds.Intersect(unshippedIds, StringComparer.Ordinal).ToArray(); + + // Assert + await Assert.That(untracked).IsEmpty(); + await Assert.That(trackedInBoth).IsEmpty(); + } + + [Test] + public async Task ReleaseTrackingFiles_SG0017ToSG0020_AreShipped(CancellationToken cancellationToken) + { + // Arrange + var shippedIds = await ReadRuleIdsAsync("AnalyzerReleases.Shipped.md", cancellationToken); + + // Act + string[] expected = ["SG0017", "SG0018", "SG0019", "SG0020"]; + var missing = expected.Where(id => !shippedIds.Contains(id)).ToArray(); + + // Assert + await Assert.That(missing).IsEmpty(); + } + + static HashSet GetDescriptorIds() => + [ + .. typeof(DiagnosticLibrary) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(static field => field.FieldType == typeof(DiagnosticDescriptor)) + .Select(static field => ((DiagnosticDescriptor)field.GetValue(null)!).Id), + ]; + + static async Task> ReadRuleIdsAsync(string fileName, CancellationToken cancellationToken) + { + var path = Path.Combine(AppContext.BaseDirectory, ReleaseTrackingFolder, fileName); + var lines = await File.ReadAllLinesAsync(path, cancellationToken); + + return + [ + .. lines + .Select(static line => RuleIdPattern().Match(line)) + .Where(static match => match.Success) + .Select(static match => match.Groups["id"].Value), + ]; + } +} diff --git a/src/tests/SourceGeneration.UnitTests/SourceGeneration.UnitTests.csproj b/src/tests/SourceGeneration.UnitTests/SourceGeneration.UnitTests.csproj index ed5e822..9f261f5 100644 --- a/src/tests/SourceGeneration.UnitTests/SourceGeneration.UnitTests.csproj +++ b/src/tests/SourceGeneration.UnitTests/SourceGeneration.UnitTests.csproj @@ -18,4 +18,18 @@ ReferenceOutputAssembly="true" /> + + + + + +