Skip to content

Rethink packaging service: per-build static NuGet feeds in blob storage #17580

@mitchdenny

Description

@mitchdenny

Summary

The Aspire CLI's channel / PackagingService layer is one of the largest sources of late-stage shipping bugs and shiproom risk we have. Today the CLI resolves NuGet packages through five different channel-specific code paths: nuget.org for stable; a shared rolling dnceng/public/_packaging/dotnet9 feed for daily; a SHA-derived darc-pub-microsoft-aspire-<sha> feed for staging (only synthesizable on stable-quality CLIs); a GitHub-Actions-artifact-plus-local-hive scheme for PRs (via gh run download); and a ~/.aspire/hives/*/packages sideload for local. Resolution depends on the CLI's baked identity, the project channel, feature flags, override config, and the CLI version's commit hash, with quality-matrix fan-out behind it. PR / daily builds are exercised through completely different acquisition logic than what ships to customers, so packaging bugs routinely surface for the first time during release.

Proposal

Replace all five branches with one code path:

  • Every CI build (PR, main, release/*, GA) publishes a complete, immutable, static NuGet v3 feed to public Azure Blob storage at a per-build URL: builds/<channel>/<runId>/. The feed is just files in a container; no service, no auth, no database to operate.
  • Channel names become tiny mutable pointer files: channels/daily.json, channels/staging.json, etc. Each names the current per-build feed URL for that channel. Promotion (staging → stable, recall, rollback) is a pointer flip, not a rebuild.
  • The CLI's job collapses to "fetch the pointer file → write a NuGet.config pointing at the URL it names → hand off to NuGet." The polymorphic channel field also accepts literal URLs, so PR builds and local hives use exactly the same code path with a different URL scheme.
  • Stable still resolves to nuget.org by default — the blob feed is opt-in for stable. Customer-visible behavior does not change. What changes is the internal pipeline that produces the bits stable eventually publishes.
  • Promote-not-rebuild for staging → stable. Staging IS the GA candidate; promotion is a pointer flip on already-signed bytes. Eliminates the "we tested an RC and rebuilt for GA" risk class.
  • Per-build path immutability + a separate locked-down publish workflow (the only holder of the federated identity, triggered workflow_run from main) prevents a PR from redirecting uploads or tampering with already-published feeds.

The result: almost all of today's channel-resolution machinery — the quality matrix, staging synthesis gating, SHA extraction, PR hive discovery, local hive sideload, override config keys, gh dependency in acquisition scripts — becomes deletable. Testing a PR build, a daily, a staging RC, and a stable GA all exercise the same code path with the same shape, which is precisely what we don't have today.

Side benefits

  • Version filtering and prerelease selection logic in the CLI is dramatically simplified — each channel resolves to one specific feed URL with one set of packages.
  • Concurrent agentic development with multiple local hives in different worktrees falls out for free; no machine-global "active hive" selection.
  • CLI identity moves to a co-located aspire-cli.identity.json written at install time — same binary across channels, enables local "fake stable / staging" debugging without a rebuild.
  • Acquisition scripts (get-aspire-cli*.sh / .ps1) drop their gh dependency.

Full design

The complete design, including security model, threat surface, open questions (provenance / signing of static feeds, lock-file mechanics for polyglot, identity-file schema, retention, etc.), goals / non-goals, and scope of impact lives in the spec PR:

The implementation sequencing will be worked out when we pick up the work; this issue tracks the overall effort.

Metadata

Metadata

Assignees

Labels

area-engineering-systemsinfrastructure helix infra engineering repo stufftriage:bot-seenAspire triage bot has seen this issue

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions