Skip to content

Add git-based template system spec and command stubs#14763

Open
mitchdenny wants to merge 28 commits intomainfrom
git-templates-spec
Open

Add git-based template system spec and command stubs#14763
mitchdenny wants to merge 28 commits intomainfrom
git-templates-spec

Conversation

@mitchdenny
Copy link
Member

Description

Introduces the git-based template system for the Aspire CLI. This is an incremental PR that will be built up over multiple phases.

Phase 1 (this commit):

  • Adds the docs/specs/git-templates.md specification document describing the full architecture: federated template indexes, per-template manifests, resolution pipeline, application engine, caching, security model, and CLI integration.
  • Stubs out the aspire template command tree behind the gitTemplatesEnabled feature flag:
    • aspire template list — list available templates
    • aspire template search <keyword> — search templates by keyword
    • aspire template refresh — refresh template cache
    • aspire template new [path] — scaffold a new template manifest
    • aspire template new-index [path] — scaffold a new template index
  • All commands currently output stub messages. Enable with: aspire config set features.gitTemplatesEnabled true

This is not a replacement for dotnet new. The git-based template system is additive — it provides polyglot-friendly, community-friendly template discovery and application alongside the existing .NET template engine.

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No
  • Does the change require an update in our Aspire docs?
    • Yes
    • No

Copilot AI review requested due to automatic review settings February 27, 2026 09:02
@github-actions
Copy link
Contributor

github-actions bot commented Feb 27, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 14763

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 14763"

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces the initial (feature-flagged) surface area for a git-based template system in the Aspire CLI, along with a comprehensive architecture specification to guide subsequent implementation phases.

Changes:

  • Added docs/specs/git-templates.md describing the proposed git-based template architecture, security model, caching, and CLI integration.
  • Added stub aspire template command group and subcommands gated behind features.gitTemplatesEnabled.
  • Wired feature flag metadata and DI registrations for the new command stubs.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/Aspire.Cli/Program.cs Registers the new template command stubs in DI.
src/Aspire.Cli/KnownFeatures.cs Adds gitTemplatesEnabled feature flag and metadata.
src/Aspire.Cli/Commands/RootCommand.cs Adds the aspire template command group to the root command when the feature flag is enabled.
src/Aspire.Cli/Commands/Template/BaseTemplateSubCommand.cs Adds a shared base for template subcommands (update notifications disabled).
src/Aspire.Cli/Commands/Template/TemplateCommand.cs Adds the aspire template parent command (stub behavior shows help).
src/Aspire.Cli/Commands/Template/TemplateListCommand.cs Adds stub aspire template list.
src/Aspire.Cli/Commands/Template/TemplateSearchCommand.cs Adds stub aspire template search <keyword>.
src/Aspire.Cli/Commands/Template/TemplateRefreshCommand.cs Adds stub aspire template refresh.
src/Aspire.Cli/Commands/Template/TemplateNewManifestCommand.cs Adds stub aspire template new [path].
src/Aspire.Cli/Commands/Template/TemplateNewIndexCommand.cs Adds stub aspire template new-index [path].
docs/specs/git-templates.md Adds the full git-templates specification document.

TelemetryCommand telemetryCommand,
DocsCommand docsCommand,
SdkCommand sdkCommand,
TemplateCommand templateCommand,
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RootCommand is in Aspire.Cli.Commands, and there is already an existing Aspire.Cli.Commands.TemplateCommand type (used for per-template commands). The newly added git-template command group is Aspire.Cli.Commands.Template.TemplateCommand, but this constructor parameter will bind to the existing Aspire.Cli.Commands.TemplateCommand instead. That type can't be created by DI (it requires an ITemplate and callback), so CLI startup will fail. Fix by renaming the new command type (e.g., GitTemplateCommand) or by explicitly qualifying/importing Aspire.Cli.Commands.Template and ensuring the parameter type is the new command-group command.

Suggested change
TemplateCommand templateCommand,
Template.TemplateCommand templateCommand,

Copilot uses AI. Check for mistakes.
Subcommands.Add(sdkCommand);
}

if (featureFlags.IsFeatureEnabled(KnownFeatures.GitTemplatesEnabled, false))
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the template command group introduces new DI requirements for RootCommand. The CLI host (Program.cs) registers these new command types, but the unit-test service registration helper (tests/Aspire.Cli.Tests/Utils/CliTestHelper) currently mirrors command registrations and will fail to resolve RootCommand unless it is updated to also register the new template command + subcommands. Please update the test DI registrations (or make this dependency optional/lazy) to keep CLI unit tests passing.

Suggested change
if (featureFlags.IsFeatureEnabled(KnownFeatures.GitTemplatesEnabled, false))
if (featureFlags.IsFeatureEnabled(KnownFeatures.GitTemplatesEnabled, false) &&
templateCommand is not null)

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Contributor

github-actions bot commented Feb 27, 2026

🎬 CLI E2E Test Recordings

The following terminal recordings are available for commit 89b3199:

Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AgentInitCommand_WithMalformedMcpJson_ShowsErrorAndExitsNonZero ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
CreateAndDeployToDockerCompose ▶️ View Recording
CreateAndDeployToDockerComposeInteractive ▶️ View Recording
CreateAndPublishToKubernetes ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateEmptyAppHostProject ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateStartWaitAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ▶️ View Recording
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording

📹 Recordings uploaded automatically from CI run #22574198507

@mitchdenny mitchdenny force-pushed the git-templates-spec branch from 1b5399e to 86bbe8b Compare March 2, 2026 01:18
@mitchdenny mitchdenny changed the base branch from main to release/13.2 March 2, 2026 01:18
@mitchdenny mitchdenny added this to the 13.3 milestone Mar 3, 2026
Mitch Denny and others added 21 commits March 3, 2026 13:34
Comprehensive spec for a git-native, federated template system for the
Aspire CLI. Templates are working Aspire apps personalized via string
substitution, hosted in git repositories with federated discovery.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stub out the 'aspire template' command group behind the
gitTemplatesEnabled feature flag. Commands added:
- aspire template list
- aspire template search <keyword>
- aspire template refresh
- aspire template new [path]
- aspire template new-index [path]

All commands output stub messages. The command tree is hidden
by default and can be enabled with:
  aspire config set features.gitTemplatesEnabled true

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Qualify Template.TemplateCommand in RootCommand constructor to
disambiguate from the existing Aspire.Cli.Commands.TemplateCommand
(used for dotnet new-style template commands).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Define C# models for aspire-template.json and aspire-template-index.json:
- GitTemplateManifest, GitTemplateIndex, GitTemplateVariable,
  GitTemplateSubstitutions, and related types
- GitTemplateJsonContext for source-generated JSON serialization

Implement interactive scaffolding in template commands:
- 'aspire template new [path]' prompts for name, display name,
  description, and language, then writes aspire-template.json
  with starter variables and substitution rules
- 'aspire template new-index [path]' prompts for publisher info
  and writes aspire-template-index.json with a placeholder entry

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The publisher URL is the GitHub URL the index was fetched from,
so there's no need to prompt for it. The command now writes a
minimal index file with a placeholder template entry and no
interactive prompts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Strip displayName, description, and minAspireVersion from the
schema models and scaffolding commands. Keep the models minimal —
these can be added back incrementally when needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement the template index resolution pipeline:
- GitTemplateSource: identifies an index source (repo URL + ref)
- GitTemplateCache: local file cache with TTL in ~/.aspire/cache/git-templates/
- IGitTemplateIndexService / GitTemplateIndexService: fetches indexes
  from GitHub via raw.githubusercontent.com, walks include graph with
  cycle detection and depth limit (5), caches results

Wire up commands to the index service:
- 'aspire template list' fetches and displays all templates in a table
- 'aspire template search <keyword>' filters by name/tags
- 'aspire template refresh' clears cache and re-fetches

Sources are configured via:
- templates.indexes.default.repo (defaults to dotnet/aspire)
- templates.indexes.default.ref (defaults to release/latest)
- templates.indexes.<name>.repo for additional sources

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement the core template application pipeline:
- TemplateExpressionEvaluator: evaluates {{var}} and {{var | filter}}
  expressions with filters: lowercase, uppercase, kebabcase,
  snakecase, camelcase, pascalcase
- IGitTemplateEngine / GitTemplateEngine: applies a template by
  copying files from a template directory to an output directory
  with filename and content substitutions, conditional file
  exclusion, binary file detection (extension allowlist + null-byte
  sniffing), and post-creation message display

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add GitTemplate and GitTemplateFactory to make git-hosted templates
appear alongside dotnet new templates in 'aspire new':
- GitTemplate: ITemplate that clones a template repo (sparse checkout),
  prompts for variables, and applies via GitTemplateEngine
- GitTemplateFactory: ITemplateFactory that yields GitTemplate
  instances from the resolved index
- Registered via TryAddEnumerable alongside DotNetTemplateFactory

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Local paths (absolute, relative, or Windows drive paths) are now
supported as index sources. This enables template development
workflows:

  aspire config set templates.indexes.dev.repo /path/to/my-templates

Local indexes are never cached — they're always read fresh from
disk so changes are reflected immediately.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GetAllConfigurationAsync returns dot-separated keys (e.g.
templates.indexes.dev.repo) but the code was filtering for
colon-separated keys (templates:indexes:dev:repo). Fixed to
use dot notation consistently.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add Telemetry.StartDiagnosticActivity() to list, search, refresh commands
- Add try/catch with user-friendly error messages for network failures
- Add sample aspire-template-index.json at repo root for default index
- Track template counts and search keywords in telemetry tags

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
For local template sources, FetchTemplateAsync was incorrectly using git clone
with sparse checkout, which doesn't work for local directory-based templates.
Now directly copies files from the resolved local path, skipping .git directories.

Also adds IsLocalPath and CopyDirectoryRecursive helpers to GitTemplate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements IGitHubCliRunner interface wrapping the 'gh' CLI to:
- Check if gh is installed and authenticated
- Get the authenticated user's username
- List the user's GitHub organizations
- Probe whether owner/aspire-templates repos exist

GitTemplateIndexService now auto-discovers template sources by probing
{username}/aspire-templates and {org}/aspire-templates repos in parallel.
Controlled via templates.enablePersonalDiscovery and
templates.enableOrgDiscovery config values (both default to true).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Discovered (personal/org) repos were using the official 'release/latest'
default ref, which doesn't exist on community repos. Now uses 'HEAD'
(resolves to repo's default branch) for non-official sources. Only the
official dotnet/aspire source uses 'release/latest'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…upport

- Add LocalizableString type for inline culture-specific translations
  (string or object with culture keys in aspire-template.json)
- Add DisplayName/Description to GitTemplateVariable and GitTemplateVariableChoice
- Add DisplayName, Scope to GitTemplateManifest
- Add Scope to GitTemplateIndexEntry
- Implement type-aware prompting in GitTemplate:
  string→text prompt with regex, boolean→confirm, choice→selection, integer→min/max
- Update GitTemplateFactory to filter by scope for GetTemplates/GetInitTemplates
- Fix GitTemplate to implement full ITemplate interface (Runtime, SupportsLanguage, etc.)
- Fix KnownEmoji usage in template commands after rebase
- Update spec with scope and localization documentation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mplate services

The test helper was missing registrations for Commands.Template.*,
IGitTemplateIndexService, IGitTemplateEngine, IGitHubCliRunner, and
GitTemplateCache that were added to Program.cs by the git templates branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Git template variables can now be provided as --key value pairs on the
command line (e.g., aspire new my-template --useRedis true --dbProvider
postgres). Variables not provided on the CLI are still prompted
interactively.

- ApplyOptions sets TreatUnmatchedTokensAsErrors = false to accept
  passthrough tokens
- ParseUnmatchedTokens extracts --key value pairs, bare --flag as true
- TryGetCliValue matches both camelCase and kebab-case naming
- ValidateCliValue applies type/regex/min-max/choice validation
- Updated spec with CLI variable binding documentation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously GitTemplate silently used the current directory name as the
project name and derived the output path. Now it interactively prompts
for both values (with sensible defaults) when --name and --output are
not provided on the command line.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Templates can define postInstructions blocks that are displayed after
template application. Each block has:
- heading: slug line displayed with # prefix styling
- priority: 'primary' (rocket emoji, bold) or 'secondary' (info emoji, dim)
- lines: instruction text with {{variable}} placeholder substitution
- condition: optional 'var == value' or 'var != value' expression

Primary instructions are rendered first, then secondary. Blocks whose
condition evaluates to false are omitted entirely.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mitch Denny and others added 7 commits March 3, 2026 13:34
Introduces the 'aspire template test' command that generates the full
cartesian product of all variable combinations for a template and applies
each one, reporting pass/fail results.

Features:
- Template resolution from local path, index, or cwd
- testValues property on variables for explicit test value control
- Automatic fallback: boolean=true/false, choice=all values, etc.
- --dry-run to preview combinations without applying
- --json for machine-readable output
- Descriptive output subdirectories per combination

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When no path argument is provided and no local template files exist,
the command now fetches the template index and presents an interactive
selection prompt filtered to git templates. Selected templates are
cloned to a temp directory, tested, then cleaned up.

Changes:
- --output is now optional (defaults to current directory)
- --name can select from the remote template index
- Added FetchAsync to IGitTemplateEngine for cloning templates
- Added git clone/sparse-checkout logic to GitTemplateEngine

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Output directories are now named like bob0, bob1, ... bob9 using
the template name and a zero-based index.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After applying each template variant, rewrites port numbers in
launchSettings.json and apphost.run.json files so each variant
uses unique ports. Uses deterministic seeding per variant index
for reproducibility, and caches port mappings within a variant
so the same original port maps to the same replacement.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Each template subcommand now overrides UpdateNotificationsEnabled
directly instead of going through an intermediate base class.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… test

Documents all recently implemented features:
- postInstructions: structured instruction blocks with priority,
  conditions, and variable substitution
- testValues: explicit test values for template test matrix generation
- aspire template test command: full documentation with options
- Variable field reference table with all properties
- scope field on index entries

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
181 test cases across 8 test files covering:

- TemplateExpressionEvaluatorTests: 26 tests for {{variable}} substitution,
  filters (lowercase, uppercase, kebabcase, snakecase, camelcase, pascalcase),
  word splitting edge cases, unresolved variables, special characters
- LocalizableStringTests: 14 tests for plain string, localized objects,
  culture fallback (exact/parent/first entry), case-insensitive keys,
  JSON deserialization of both string and object forms
- GitTemplateManifestSerializationTests: 33 tests for manifest parsing
  including all variable types (string/boolean/choice/integer), validation,
  testValues, substitutions, conditionalFiles, postMessages, postInstructions,
  scope, localized displayName, roundtrip serialization
- GitTemplateIndexSerializationTests: 10 tests for index parsing including
  templates, publisher, includes (federation), schema field, roundtrip
- JsonConverterTests: 24 tests for JsonObjectConverter and
  JsonObjectListConverter polymorphic deserialization (string/bool/int/null)
- GitTemplateEngineTests: 21 tests for end-to-end template application
  including content/filename substitution, filters, conditional files,
  binary file handling, excluded dirs, nested structures, postMessages
- GitTemplateLogicTests: 35 tests for condition evaluation (==, !=, truthy),
  variable substitution, CLI token parsing, validation, kebab-case conversion
- GitTemplateSchemaCompatibilityTests: 18 tests with real-world template
  manifests to ensure backward compatibility as schema evolves

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny mitchdenny force-pushed the git-templates-spec branch from 89b3199 to 3ae8557 Compare March 3, 2026 02:34
@mitchdenny mitchdenny changed the base branch from release/13.2 to main March 3, 2026 02:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants