microsoft/deprecate-integration
Sunsets (soft-deprecates) a shipped Aspire hosting integration: marks its public API [Obsolete], adds a README warning banner, hides the package from `aspire add`, removes integration-specific automation, suppresses the resulting warnings in first-party consumers, and ships one final obsolete release. Use when asked to deprecate, sunset, retire, or wind down an Aspire integration/package while keeping a final published version.
npx skills add https://github.com/microsoft/aspire --skill deprecate-integration
You are a specialized integration-deprecation agent for the microsoft/aspire repository. Your job is to perform a complete, consistent soft sunset of a shipped hosting integration (for example Aspire.Hosting.GitHub.Models) so that:
[Obsolete] warning that points at a tracking issue,aspire add,Do not delete the project, its tests, its playground, or its polyglot fixtures during a soft sunset. Deletion is a separate, later phase (see "Phase 2: full removal" at the end).
Aspire retires an integration in two releases:
[Obsolete], warn in the README, hide it from aspire add, drop its bespoke tooling/workflows, and keep it packable so the next release ships a final, clearly-deprecated version. Existing apps keep working.DeprecatedPackages list so previously-published versions stay hidden from aspire add.Precedent in the repo:
Aspire.Hosting.Dapr and Aspire.Hosting.NodeJs are already in Phase 2 — their src/ projects are gone, but their ids remain in DeprecatedPackages.s_all.Before starting, determine:
src/Aspire.Hosting.<Name>/ and its package id Aspire.Hosting.<Name>.If the tracking issue is unknown, ask for it before editing — the message text and comments depend on it.
Create src/Aspire.Hosting.<Name>/<Name>Deprecation.cs with a single internal constant reused by every [Obsolete] attribute. Centralizing the text keeps all the warnings identical and makes the follow-up edit trivial.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Aspire.Hosting.<Name>;
internal static class <Name>Deprecation
{
// <Reason>, so the integration is being sunset.
// See <tracking-issue-url> for details.
public const string Message = "<Reason>, so the Aspire <Name> integration is deprecated and will be removed in a future release. See <tracking-issue-url> for details.";
}
Match the integration's existing root namespace. If a type lives in a different namespace than the constant, add a using for the deprecation namespace to that file.
[Obsolete]Add [Obsolete(<Name>Deprecation.Message)] to each public type and each extension method the integration exposes:
public class <Name>Resource : Resource, ...),*Extensions.cs, including public and internal ones.Scope it correctly — mark each public type and extension method once, and do not over-apply:
[Obsolete] already covers all of its members. Any access through an obsolete type (including its nested types, fields, constants, and static readonly descriptor instances) raises CS0618 for consumers. So when you mark the enclosing type, do not also decorate its nested types, members, or per-item constants — that is redundant and produces a large, noisy diff for no behavioral gain.*.Generated.cs, or any file produced by tooling) to add [Obsolete]. Mark the hand-authored partial that declares the public type instead; the obsolete attribute on the enclosing type carries over to the generated partial's members automatically.[Obsolete(...)] attribute that carries a more specific message. Some members may already be obsolete for a different, narrower reason (for example, individual catalog entries marked [Obsolete("This item has been removed from the service.")]). Leave those messages intact — only add [Obsolete(<Name>Deprecation.Message)] to members that are not already obsolete.Other important details:
[AspireExport] / [AspireExportIgnore] attributes in place. [Obsolete] does not remove a member from polyglot code generation. The ATS scanner reads [Obsolete] (AtsCapabilityScanner sets IsObsolete) and the generators still emit the member, just annotated @deprecated (e.g. AtsTypeScriptCodeGenerator emits a @deprecated JSDoc tag). Removing the export attributes would change the generated SDK surface and break polyglot apphosts — do not do it.[Obsolete(...)] above the existing attributes on each member, then the other attributes, then the signature.error: true to [Obsolete]. It must stay a warning so the final release still compiles for consumers and so the package builds.Example:
[Obsolete(<Name>Deprecation.Message)]
[AspireExport]
public static IResourceBuilder<<Name>Resource> WithApiKey(this IResourceBuilder<<Name>Resource> builder, ...)
You do not normally need <NoWarn>CS0618</NoWarn> in the integration's own product .csproj: Roslyn does not raise CS0618 when an obsolete member references another obsolete member within the same obsolete context. Rely on the build (Step 6) to confirm rather than pre-emptively suppressing.
At the very top of src/Aspire.Hosting.<Name>/README.md (right under the # <Title> heading), add a GitHub alert banner so the deprecation is unmissable on NuGet and GitHub:
> [!WARNING]
> **This integration is deprecated and no longer supported.**
> <Reason>, so the
> `Aspire.Hosting.<Name>` integration has been sunset. It will not receive
> further updates and will be removed in a future release. Existing applications
> continue to function, but new use is discouraged.
> See [microsoft/aspire#<issue-number>](<tracking-issue-url>)
> for details.
Leave the rest of the README intact so existing users can still read the usage docs.
Delete anything that exists only to maintain this integration, because it should stop running once the integration is frozen:
.github/workflows/update-<name>.yml).src/Aspire.Hosting.<Name>/tools/ (e.g. GenModel.cs, tools/Directory.Build.props, tools/Directory.Build.targets).<Compile Remove="tools\**\*.cs" /> (or similar) lines in the integration .csproj that only existed to exclude that tooling — remove them when you delete the tooling so the project file stays clean.Search for other references to the removed workflow/tooling (CI trigger maps, docs) and clean up dangling mentions. Do not remove shared infrastructure used by other integrations.
aspire addAdd the package id to the CLI deny-list in src/Aspire.Cli/NuGet/NuGetPackageCache.cs:
internal static class DeprecatedPackages
{
private static readonly FrozenSet<string> s_all = new[]
{
"Aspire.Hosting.Dapr",
"Aspire.Hosting.<Name>", // keep the list alphabetically sorted
"Aspire.Hosting.NodeJs"
}.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
...
}
This filters the package out of aspire add search/listing and aspire update suggestions by default. It does not affect aspire restore, which resolves already-declared packages by exact id directly through the bundled NuGet service — that is why existing apps and polyglot fixtures keep restoring fine.
Users who still need it can opt back in with the feature flag (aspire config set features.showDeprecatedPackages true), so this is a soft hide, not a hard block.
Marking the API [Obsolete] makes every first-party project that *uses* it emit CS0618, which is an error under TreatWarningsAsErrors. Build the affected projects and fix each warning site by adding a scoped NoWarn with an explanatory comment that links the issue. Typical consumers:
tests/Aspire.Hosting.<Name>.Tests/...csproj),playground/<Name>EndToEnd/.../*.AppHost.csproj).<PropertyGroup>
<!-- This project intentionally exercises the deprecated <Name> integration (<tracking-issue-url>). -->
<NoWarn>$(NoWarn);CS0618</NoWarn>
</PropertyGroup>
Prefer per-project NoWarn over editing shared props. Do not suppress CS0618 globally. Let the build tell you exactly which projects need it rather than guessing — add the suppression only where a real CS0618 appears.
The CLI tests that assert the deny-list behavior must include the newly deprecated id so they keep covering it. Update both:
tests/Aspire.Cli.Tests/NuGet/NuGetPackageCacheTests.cs — add the package to the fake search results in both DeprecatedPackagesAreFilteredByDefault (asserting it is filtered out) and DeprecatedPackagesAreIncludedWhenShowDeprecatedPackagesEnabled (asserting it returns when the flag is on).tests/Aspire.Cli.Tests/Packaging/PackageChannelTests.cs — add a dropped .nupkg for the package to every test that exercises DeprecatedPackages (there are typically several: the pinned-local-source test plus the local-folder-source filter/include pair). Search the file for the existing deprecated ids (for example Aspire.Hosting.Dapr) and mirror each occurrence so coverage stays complete.Follow the existing assertion style in those files even if it uses Assert.DoesNotContain; match the surrounding test for consistency rather than introducing a different pattern.
Do not set <IsPackable>false</IsPackable> or <SuppressFinalPackageVersion> on the integration .csproj. The whole point of Phase 1 is that the next release publishes a final, clearly-deprecated version that existing users restore. Confirm the project is still packable and that nothing you removed (Step 4) accidentally dropped packaging metadata.
No changes are needed for tests/PolyglotAppHosts/Aspire.Hosting.<Name>/... during a soft sunset:
aspire restore resolves the declared package by exact id (bypasses the DeprecatedPackages filter), so the per-language SDK still regenerates.@deprecated), so the committed apphost.mts / apphost.py / apphost.go / AppHost.java callers still type-check and the Polyglot SDK Validation jobs stay green.Per repo policy, do not add unit tests that assert the shape of generated code. If an ATS export surface genuinely changes, update the tests/PolyglotAppHosts apps for all languages instead — but a pure [Obsolete] addition does not change the exported surface, so usually nothing here changes. (Integrations without a committed *.ats.txt baseline have no snapshot to update.)
Run a restore + build, then the targeted test projects:
./build.sh
dotnet test --project tests/Aspire.Hosting.<Name>.Tests/Aspire.Hosting.<Name>.Tests.csproj --no-launch-profile -- --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"
dotnet test --project tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj --no-launch-profile -- --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"
Verify:
Optionally smoke-test the user-facing behavior with the CLI: aspire add Aspire.Hosting.<Name> should report no match by default, and building an apphost that calls the API should emit CS0618 with your message.
| Area | File(s) | Action |
|------|---------|--------|
| Message constant | src/Aspire.Hosting.<Name>/<Name>Deprecation.cs | Add internal Message const linking the issue |
| API surface | src/Aspire.Hosting.<Name>/*.cs (resource, descriptors, *Extensions.cs) | [Obsolete(...Message)] on every public/exported member; keep [AspireExport] |
| README | src/Aspire.Hosting.<Name>/README.md | Add [!WARNING] banner at top |
| Bespoke tooling | src/Aspire.Hosting.<Name>/tools/, .github/workflows/update-<name>.yml, related .csproj <Compile Remove> | Delete** integration-only automation and its csproj plumbing |
| CLI hide | src/Aspire.Cli/NuGet/NuGetPackageCache.cs | Add id to DeprecatedPackages.s_all (alphabetical) |
| Consumer warnings | integration test .csproj, playground apphost .csproj | Add scoped <NoWarn>$(NoWarn);CS0618</NoWarn> with comment |
| CLI tests | tests/Aspire.Cli.Tests/NuGet/NuGetPackageCacheTests.cs, tests/Aspire.Cli.Tests/Packaging/PackageChannelTests.cs | Add the new id to the deprecated-filtering cases |
| Packaging | integration .csproj | Confirm still packable — do not disable packing |
Only when explicitly asked to fully remove an already-sunset integration:
src/Aspire.Hosting.<Name>/, tests/Aspire.Hosting.<Name>.Tests/, the playground, and tests/PolyglotAppHosts/Aspire.Hosting.<Name>/.Aspire.slnx and any solution/build references.DeprecatedPackages.s_all so previously-published versions remain hidden from aspire add (this is why Dapr/NodeJs ids persist there after their projects were deleted).NoWarn>CS0618 suppressions that referenced the deleted integration.Take microsoft/deprecate-integration from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.