-
Notifications
You must be signed in to change notification settings - Fork 851
Add git-based template system spec and command stubs #14763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mitchdenny
wants to merge
28
commits into
main
Choose a base branch
from
git-templates-spec
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
c36f16a
Add git-based template system spec
ab8184f
Add aspire template command tree (Phase 1)
bcc9ba7
Fix TemplateCommand DI resolution ambiguity
f4d4396
Add git template schema models and scaffolding commands (Phase 2)
2e8dde7
Simplify new-index: remove publisher prompts
a52d270
Remove displayName/description from models for now
f6f1297
Add index resolution, caching, and live commands (Phase 3)
852c189
Add template application engine (Phase 4)
091f3b3
Integrate GitTemplateFactory into aspire new (Phase 5)
900f851
Support local paths in template index sources
fda67d9
Fix config key format in index source discovery
06fa720
Phase 6: Add telemetry, error handling, and sample template index
90e0c87
Fix local template application: copy files directly instead of git clone
c2d76dc
Add GitHub CLI auto-discovery of aspire-templates repos
15c2bb2
Fix default ref for discovered template repos
8fc0d6e
Fix markdown lint: add language to fenced code blocks
d8ff1a4
Add type-aware variable prompting, template scope, and localization s…
899bccb
Fix CI: add missing DI registrations for template commands and git te…
b7256ec
Enable CLI variable binding for git template variables
fd83356
Prompt for project name and output directory when not provided
0ca2103
Add postInstructions rendering for git templates
f21805d
Add aspire template test command for matrix testing
a51fa40
Enable interactive template selection for template test command
3165367
Simplify template test output folder naming to {templateName}{index}
b5b89bc
Randomize ports per test variant in template test command
78623d0
Remove BaseTemplateSubCommand, inherit BaseCommand directly
8ffa25c
Update git-templates spec with postInstructions, testValues, template…
3ae8557
Add comprehensive git template language test suite
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "$schema": "https://aka.ms/aspire/template-index-schema/v1", | ||
| "version": 1, | ||
| "templates": [ | ||
| { | ||
| "name": "aspire-starter", | ||
| "path": "templates/aspire-starter", | ||
| "language": "csharp", | ||
| "tags": ["starter", "web", "api"] | ||
| } | ||
| ] | ||
| } |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.CommandLine; | ||
| using System.CommandLine.Help; | ||
| using Aspire.Cli.Configuration; | ||
| using Aspire.Cli.Interaction; | ||
| using Aspire.Cli.Telemetry; | ||
| using Aspire.Cli.Utils; | ||
|
|
||
| namespace Aspire.Cli.Commands.Template; | ||
|
|
||
| internal sealed class TemplateCommand : BaseCommand | ||
| { | ||
| internal override HelpGroup HelpGroup => HelpGroup.ToolsAndConfiguration; | ||
|
|
||
| public TemplateCommand( | ||
| TemplateListCommand listCommand, | ||
| TemplateSearchCommand searchCommand, | ||
| TemplateRefreshCommand refreshCommand, | ||
| TemplateNewManifestCommand newManifestCommand, | ||
| TemplateNewIndexCommand newIndexCommand, | ||
| TemplateTestCommand testCommand, | ||
| IFeatures features, | ||
| ICliUpdateNotifier updateNotifier, | ||
| CliExecutionContext executionContext, | ||
| IInteractionService interactionService, | ||
| AspireCliTelemetry telemetry) | ||
| : base("template", "Manage git-based Aspire templates", features, updateNotifier, executionContext, interactionService, telemetry) | ||
| { | ||
| Subcommands.Add(listCommand); | ||
| Subcommands.Add(searchCommand); | ||
| Subcommands.Add(refreshCommand); | ||
| Subcommands.Add(newManifestCommand); | ||
| Subcommands.Add(newIndexCommand); | ||
| Subcommands.Add(testCommand); | ||
| } | ||
|
|
||
| protected override bool UpdateNotificationsEnabled => false; | ||
|
|
||
| protected override Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) | ||
| { | ||
| new HelpAction().Invoke(parseResult); | ||
| return Task.FromResult(ExitCodeConstants.InvalidCommand); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.CommandLine; | ||
| using Aspire.Cli.Configuration; | ||
| using Aspire.Cli.Interaction; | ||
| using Aspire.Cli.Telemetry; | ||
| using Aspire.Cli.Templating.Git; | ||
| using Aspire.Cli.Utils; | ||
| using Spectre.Console; | ||
|
|
||
| namespace Aspire.Cli.Commands.Template; | ||
|
|
||
| internal sealed class TemplateListCommand( | ||
| IGitTemplateIndexService indexService, | ||
| IFeatures features, | ||
| ICliUpdateNotifier updateNotifier, | ||
| CliExecutionContext executionContext, | ||
| IInteractionService interactionService, | ||
| AspireCliTelemetry telemetry) | ||
| : BaseCommand("list", "List available templates from all configured sources", features, updateNotifier, executionContext, interactionService, telemetry) | ||
| { | ||
| protected override bool UpdateNotificationsEnabled => false; | ||
|
|
||
| protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) | ||
| { | ||
| using var activity = Telemetry.StartDiagnosticActivity("template-list"); | ||
|
|
||
| IReadOnlyList<ResolvedTemplate> templates; | ||
| try | ||
| { | ||
| templates = await InteractionService.ShowStatusAsync( | ||
| ":magnifying_glass_tilted_right: Fetching templates...", | ||
| () => indexService.GetTemplatesAsync(cancellationToken: cancellationToken)); | ||
| } | ||
| catch (Exception ex) when (ex is not OperationCanceledException) | ||
| { | ||
| Telemetry.RecordError("Failed to fetch template list", ex); | ||
| InteractionService.DisplayError("Failed to fetch templates. Check your network connection and try again."); | ||
| return 1; | ||
| } | ||
|
|
||
| activity?.AddTag("template.count", templates.Count); | ||
|
|
||
| if (templates.Count == 0) | ||
| { | ||
| InteractionService.DisplayMessage(KnownEmojis.Information, "No templates found. Try 'aspire template refresh' to update the index cache."); | ||
| return ExitCodeConstants.Success; | ||
| } | ||
|
|
||
| var table = new Table(); | ||
| table.Border(TableBorder.Rounded); | ||
| table.AddColumn(new TableColumn("[bold]Name[/]").NoWrap()); | ||
| table.AddColumn(new TableColumn("[bold]Source[/]")); | ||
| table.AddColumn(new TableColumn("[bold]Language[/]")); | ||
| table.AddColumn(new TableColumn("[bold]Tags[/]")); | ||
|
|
||
| foreach (var t in templates) | ||
| { | ||
| table.AddRow( | ||
| t.Entry.Name.EscapeMarkup(), | ||
| t.Source.Name.EscapeMarkup(), | ||
| (t.Entry.Language ?? "").EscapeMarkup(), | ||
| t.Entry.Tags is { Count: > 0 } ? string.Join(", ", t.Entry.Tags).EscapeMarkup() : ""); | ||
| } | ||
|
|
||
| AnsiConsole.Write(table); | ||
| return ExitCodeConstants.Success; | ||
| } | ||
| } |
78 changes: 78 additions & 0 deletions
78
src/Aspire.Cli/Commands/Template/TemplateNewIndexCommand.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.CommandLine; | ||
| using System.Text.Json; | ||
| using Aspire.Cli.Configuration; | ||
| using Aspire.Cli.Interaction; | ||
| using Aspire.Cli.Telemetry; | ||
| using Aspire.Cli.Templating.Git; | ||
| using Aspire.Cli.Utils; | ||
|
|
||
| namespace Aspire.Cli.Commands.Template; | ||
|
|
||
| internal sealed class TemplateNewIndexCommand : BaseCommand | ||
| { | ||
| private static readonly Argument<string?> s_pathArgument = new("path") | ||
| { | ||
| Description = "Directory to create aspire-template-index.json in (defaults to current directory)", | ||
| Arity = ArgumentArity.ZeroOrOne | ||
| }; | ||
|
|
||
| public TemplateNewIndexCommand( | ||
| IFeatures features, | ||
| ICliUpdateNotifier updateNotifier, | ||
| CliExecutionContext executionContext, | ||
| IInteractionService interactionService, | ||
| AspireCliTelemetry telemetry) | ||
| : base("new-index", "Scaffold a new aspire-template-index.json index file", features, updateNotifier, executionContext, interactionService, telemetry) | ||
| { | ||
| Arguments.Add(s_pathArgument); | ||
| } | ||
|
|
||
| protected override bool UpdateNotificationsEnabled => false; | ||
|
|
||
| protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) | ||
| { | ||
| var targetDir = parseResult.GetValue(s_pathArgument) ?? Directory.GetCurrentDirectory(); | ||
| targetDir = Path.GetFullPath(targetDir); | ||
|
|
||
| var outputPath = Path.Combine(targetDir, "aspire-template-index.json"); | ||
|
|
||
| if (File.Exists(outputPath)) | ||
| { | ||
| var overwrite = await InteractionService.ConfirmAsync( | ||
| $"aspire-template-index.json already exists in {targetDir}. Overwrite?", | ||
| defaultValue: false, | ||
| cancellationToken: cancellationToken).ConfigureAwait(false); | ||
|
|
||
| if (!overwrite) | ||
| { | ||
| InteractionService.DisplayMessage(KnownEmojis.Information, "Cancelled."); | ||
| return ExitCodeConstants.Success; | ||
| } | ||
| } | ||
|
|
||
| var index = new GitTemplateIndex | ||
| { | ||
| Schema = "https://aka.ms/aspire/template-index-schema/v1", | ||
| Templates = | ||
| [ | ||
| new GitTemplateIndexEntry | ||
| { | ||
| Name = "my-template", | ||
| Path = "templates/my-template" | ||
| } | ||
| ] | ||
| }; | ||
|
|
||
| Directory.CreateDirectory(targetDir); | ||
|
|
||
| var json = JsonSerializer.Serialize(index, GitTemplateJsonContext.RelaxedEscaping.GitTemplateIndex); | ||
| await File.WriteAllTextAsync(outputPath, json, cancellationToken).ConfigureAwait(false); | ||
|
|
||
| InteractionService.DisplaySuccess($"Created {outputPath}"); | ||
| InteractionService.DisplayMessage(KnownEmojis.Information, "Edit the file to add your templates, then run 'aspire template new' in each template directory."); | ||
| return ExitCodeConstants.Success; | ||
| } | ||
| } |
117 changes: 117 additions & 0 deletions
117
src/Aspire.Cli/Commands/Template/TemplateNewManifestCommand.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.CommandLine; | ||
| using System.Text.Json; | ||
| using System.Text.RegularExpressions; | ||
| using Aspire.Cli.Configuration; | ||
| using Aspire.Cli.Interaction; | ||
| using Aspire.Cli.Telemetry; | ||
| using Aspire.Cli.Templating.Git; | ||
| using Aspire.Cli.Utils; | ||
| using Spectre.Console; | ||
|
|
||
| namespace Aspire.Cli.Commands.Template; | ||
|
|
||
| internal sealed partial class TemplateNewManifestCommand : BaseCommand | ||
| { | ||
| private static readonly Argument<string?> s_pathArgument = new("path") | ||
| { | ||
| Description = "Directory to create aspire-template.json in (defaults to current directory)", | ||
| Arity = ArgumentArity.ZeroOrOne | ||
| }; | ||
|
|
||
| public TemplateNewManifestCommand( | ||
| IFeatures features, | ||
| ICliUpdateNotifier updateNotifier, | ||
| CliExecutionContext executionContext, | ||
| IInteractionService interactionService, | ||
| AspireCliTelemetry telemetry) | ||
| : base("new", "Scaffold a new aspire-template.json manifest", features, updateNotifier, executionContext, interactionService, telemetry) | ||
| { | ||
| Arguments.Add(s_pathArgument); | ||
| } | ||
|
|
||
| protected override bool UpdateNotificationsEnabled => false; | ||
|
|
||
| protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) | ||
| { | ||
| var targetDir = parseResult.GetValue(s_pathArgument) ?? Directory.GetCurrentDirectory(); | ||
| targetDir = Path.GetFullPath(targetDir); | ||
|
|
||
| var outputPath = Path.Combine(targetDir, "aspire-template.json"); | ||
|
|
||
| if (File.Exists(outputPath)) | ||
| { | ||
| var overwrite = await InteractionService.ConfirmAsync( | ||
| $"aspire-template.json already exists in {targetDir}. Overwrite?", | ||
| defaultValue: false, | ||
| cancellationToken: cancellationToken).ConfigureAwait(false); | ||
|
|
||
| if (!overwrite) | ||
| { | ||
| InteractionService.DisplayMessage(KnownEmojis.Information, "Cancelled."); | ||
| return ExitCodeConstants.Success; | ||
| } | ||
| } | ||
|
|
||
| var name = await InteractionService.PromptForStringAsync( | ||
| "Template name (kebab-case identifier)", | ||
| defaultValue: Path.GetFileName(targetDir)?.ToLowerInvariant(), | ||
| validator: value => KebabCasePattern().IsMatch(value) | ||
| ? ValidationResult.Success() | ||
| : ValidationResult.Error("Must be lowercase kebab-case (e.g. my-template)"), | ||
| required: true, | ||
| cancellationToken: cancellationToken).ConfigureAwait(false); | ||
|
|
||
| var canonicalName = ToPascalCase(name); | ||
|
|
||
| var manifest = new GitTemplateManifest | ||
| { | ||
| Schema = "https://aka.ms/aspire/template-schema/v1", | ||
| Name = name, | ||
| Variables = new Dictionary<string, GitTemplateVariable> | ||
| { | ||
| ["projectName"] = new() | ||
| { | ||
| Type = "string", | ||
| Required = true, | ||
| DefaultValue = canonicalName, | ||
| Validation = new GitTemplateVariableValidation | ||
| { | ||
| Pattern = "^[A-Za-z][A-Za-z0-9_.]*$", | ||
| } | ||
| } | ||
| }, | ||
| Substitutions = new GitTemplateSubstitutions | ||
| { | ||
| Filenames = new Dictionary<string, string> | ||
| { | ||
| [canonicalName] = "{{projectName}}" | ||
| }, | ||
| Content = new Dictionary<string, string> | ||
| { | ||
| [canonicalName] = "{{projectName}}", | ||
| [canonicalName.ToLowerInvariant()] = "{{projectName | lowercase}}" | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| Directory.CreateDirectory(targetDir); | ||
|
|
||
| var json = JsonSerializer.Serialize(manifest, GitTemplateJsonContext.RelaxedEscaping.GitTemplateManifest); | ||
| await File.WriteAllTextAsync(outputPath, json, cancellationToken).ConfigureAwait(false); | ||
|
|
||
| InteractionService.DisplaySuccess($"Created {outputPath}"); | ||
| return ExitCodeConstants.Success; | ||
| } | ||
|
|
||
| private static string ToPascalCase(string kebab) | ||
| { | ||
| return string.Concat(kebab.Split('-').Select(part => | ||
| part.Length == 0 ? "" : char.ToUpperInvariant(part[0]) + part[1..])); | ||
| } | ||
|
|
||
| [GeneratedRegex("^[a-z][a-z0-9]*(-[a-z0-9]+)*$")] | ||
| private static partial Regex KebabCasePattern(); | ||
| } |
48 changes: 48 additions & 0 deletions
48
src/Aspire.Cli/Commands/Template/TemplateRefreshCommand.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.CommandLine; | ||
| using Aspire.Cli.Configuration; | ||
| using Aspire.Cli.Interaction; | ||
| using Aspire.Cli.Telemetry; | ||
| using Aspire.Cli.Templating.Git; | ||
| using Aspire.Cli.Utils; | ||
|
|
||
| namespace Aspire.Cli.Commands.Template; | ||
|
|
||
| internal sealed class TemplateRefreshCommand( | ||
| IGitTemplateIndexService indexService, | ||
| IFeatures features, | ||
| ICliUpdateNotifier updateNotifier, | ||
| CliExecutionContext executionContext, | ||
| IInteractionService interactionService, | ||
| AspireCliTelemetry telemetry) | ||
| : BaseCommand("refresh", "Force refresh the template index cache", features, updateNotifier, executionContext, interactionService, telemetry) | ||
| { | ||
| protected override bool UpdateNotificationsEnabled => false; | ||
|
|
||
| protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) | ||
| { | ||
| using var activity = Telemetry.StartDiagnosticActivity("template-refresh"); | ||
|
|
||
| try | ||
| { | ||
| await InteractionService.ShowStatusAsync( | ||
| ":counterclockwise_arrows_button: Refreshing template index cache...", | ||
| async () => | ||
| { | ||
| await indexService.RefreshAsync(cancellationToken); | ||
| return 0; | ||
| }); | ||
| } | ||
| catch (Exception ex) when (ex is not OperationCanceledException) | ||
| { | ||
| Telemetry.RecordError("Failed to refresh template cache", ex); | ||
| InteractionService.DisplayError("Failed to refresh template cache. Check your network connection and try again."); | ||
| return 1; | ||
| } | ||
|
|
||
| InteractionService.DisplaySuccess("Template index cache refreshed."); | ||
| return ExitCodeConstants.Success; | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 resolveRootCommandunless 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.