From 5968e9e9c6dab9cfbfad5d060eed40e3906281f2 Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Sun, 27 Sep 2026 08:46:23 +0200 Subject: [PATCH] Test the Azure DevOps templates during package installation (#130) TestPackageInstallation only installed the four GitHub-based template variants via dotnet new, never the two azdo-* templates added in #85. It also hard-coded the list of short names, so a future variant could be forgotten again. Short names are now read from the rendered template.json files, and the azdo-* templates are installed with --organization/--projectName to exercise their symbol replacement, asserted against the generated Build/Build.cs. This also uncovered a real bug: the Azdo template's "project" symbol produced a --project switch that collides with dotnet new's own built-in --project option (used for template context evaluation), so that parameter could never actually be set. Renamed the symbol to projectName (--projectName) and updated the README and Adopt-StarterKit.ps1 accordingly. Co-Authored-By: Claude Sonnet 5 --- Adopt-StarterKit.ps1 | 2 +- README.md | 4 +- build/Build.cs | 77 +++++++++++++------ .../Source/.template.config/template.json | 2 +- 4 files changed, 58 insertions(+), 27 deletions(-) diff --git a/Adopt-StarterKit.ps1 b/Adopt-StarterKit.ps1 index c46fc08..c381adb 100644 --- a/Adopt-StarterKit.ps1 +++ b/Adopt-StarterKit.ps1 @@ -126,7 +126,7 @@ $generated = Join-Path ([System.IO.Path]::GetTempPath()) "starterkit-$Name-$([Gu $templateArguments = @("new", $Template, "--name", $Name, "--output", $generated) if ($Organization) { $templateArguments += @("--organization", $Organization) } -if ($Project) { $templateArguments += @("--project", $Project) } +if ($Project) { $templateArguments += @("--projectName", $Project) } Write-Host "Generating '$Template' for '$Name' into $generated" & dotnet @templateArguments | Out-Null diff --git a/README.md b/README.md index 107d40c..ffda781 100644 --- a/README.md +++ b/README.md @@ -119,11 +119,11 @@ dotnet new update If you are still using Azure DevOps, we got you covered, but you need the organization and project names as well. Again, choose between a binary package or a source-only package. ``` - dotnet new azdo-nuget-class-library-sln --name TheNameOfYourAwesomeLibrary --organization MyDevOpsOrganization --project MyDevOpsProject + dotnet new azdo-nuget-class-library-sln --name TheNameOfYourAwesomeLibrary --organization MyDevOpsOrganization --projectName MyDevOpsProject ``` ``` - dotnet new azdo-source-only-nuget-class-library-sln --name TheNameOfYourAwesomeLibrary --organization MyDevOpsOrganization --project MyDevOpsProject + dotnet new azdo-source-only-nuget-class-library-sln --name TheNameOfYourAwesomeLibrary --organization MyDevOpsOrganization --projectName MyDevOpsProject ``` The GitHub templates use [Dependabot](https://docs.github.com/en/code-security/dependabot) to keep the NuGet packages and GitHub Actions up-to-date. If you prefer [Renovate](https://docs.renovatebot.com/), add `--dependency-updater renovate`. You get a `.github/renovate.json` instead of `.github/dependabot.yml`, so you don't get duplicate pull requests. It groups related updates (such as all analyzers or all xUnit packages) into one pull request, updates the .NET SDK in `global.json`, and automatically merges patch updates of analyzers, test and build packages. diff --git a/build/Build.cs b/build/Build.cs index d58bac9..3c1a040 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -19,6 +19,9 @@ class Build : FalloutBuild { + static readonly string[] TemplateVariantNames = + ["Normal", "SourceOnly", "NormalOss", "SourceOnlyOss", "NormalAzdo", "SourceOnlyAzdo"]; + /// Support plugins are available for: /// - JetBrains ReSharper https://nuke.build/resharper /// - JetBrains Rider https://nuke.build/rider @@ -141,8 +144,7 @@ class Build : FalloutBuild PackageReadme = true, }); - string[] names = ["Normal", "SourceOnly", "NormalOss", "SourceOnlyOss", "NormalAzdo", "SourceOnlyAzdo"]; - foreach (string name in names) + foreach (string name in TemplateVariantNames) { (ArtifactsDirectory / "templates" / name / "PackageReadme.md").WriteAllText(readmeContents); } @@ -187,8 +189,7 @@ class Build : FalloutBuild } // Write into every template variant - string[] variantNames = ["Normal", "NormalOss", "SourceOnly", "SourceOnlyOss", "NormalAzdo", "SourceOnlyAzdo"]; - foreach (string variant in variantNames) + foreach (string variant in TemplateVariantNames) { var skillsDir = ArtifactsDirectory / "templates" / variant / ".agents" / "skills" / "csharp-guidelines"; skillsDir.CreateOrCleanDirectory(); @@ -302,46 +303,66 @@ class Build : FalloutBuild // Install the locally built package with force flag DotNet($"new install {packageFile} --force", workingDirectory: testDirectory); - // Test each template variant by creating and building a test project - string[] templateShortNames = [ - "nooss-nuget-class-library-sln", - "nooss-source-only-nuget-class-library-sln", - "oss-nuget-class-library-sln", - "oss-source-only-nuget-class-library-sln", - "oss-nuget-class-library-sln --benchmarks true", - "oss-source-only-nuget-class-library-sln --benchmarks true" - ]; - - foreach (string templateName in templateShortNames) + // Test each template variant by creating and building a test project. The short names are read from + // the rendered template.json files, so a newly added variant cannot be forgotten here. + string[] templateShortNames = TemplateVariantNames + .Select(name => ArtifactsDirectory / "templates" / name / ".template.config" / "template.json") + .Select(templateJsonFile => JsonDocument.Parse(templateJsonFile.ReadAllText()) + .RootElement.GetProperty("shortName").GetString()) + .ToArray(); + + // The Azure DevOps templates require an organization and project, which also exercises their + // symbol replacement. The OSS templates are additionally tested with the optional benchmarks flag. + string[] templateArguments = templateShortNames + .Select(shortName => shortName.StartsWith("azdo-", StringComparison.Ordinal) + ? $"{shortName} --organization TestOrganization --projectName TestProject" + : shortName) + .Concat([ + "oss-nuget-class-library-sln --benchmarks true", + "oss-source-only-nuget-class-library-sln --benchmarks true" + ]) + .ToArray(); + + foreach (string templateArgument in templateArguments) { + string shortName = templateArgument.Split(' ')[0]; + bool isAzdo = shortName.StartsWith("azdo-", StringComparison.Ordinal); + var projectTestDirectory = testDirectory / $"solution"; projectTestDirectory.DeleteDirectory(); projectTestDirectory.CreateDirectory(); - Information("Testing template: {Template}", templateName); + Information("Testing template: {Template}", templateArgument); // Create project from template - DotNet($"new {templateName:nq} --name TestLibrary --force", workingDirectory: projectTestDirectory); + DotNet($"new {templateArgument:nq} --name TestLibrary --force", workingDirectory: projectTestDirectory); // Build the generated project to ensure it compiles without errors // Note: template uses preferNameDirectory=true, so project is created in TestLibrary subdirectory var actualProjectDirectory = projectTestDirectory / "TestLibrary"; DotNet("build", workingDirectory: actualProjectDirectory); - Information("Successfully built project from template: {Template}", templateName); + Information("Successfully built project from template: {Template}", templateArgument); // Check if the basic project structure was created correctly if ((actualProjectDirectory / $"TestLibrary.slnx").FileExists() || (actualProjectDirectory / $"TestLibrary").DirectoryExists()) { - Information("Project structure was created successfully for template: {Template}", templateName); + Information("Project structure was created successfully for template: {Template}", templateArgument); } else { - Error($"Template {templateName} failed to create proper project structure"); + Error($"Template {templateArgument} failed to create proper project structure"); } - AssertDependencyUpdater(actualProjectDirectory, expectDependabot: true); + if (isAzdo) + { + AssertAzdoReplacements(actualProjectDirectory, "TestOrganization", "TestProject"); + } + else + { + AssertDependencyUpdater(actualProjectDirectory, expectDependabot: true); + } } // Verify that choosing Renovate replaces the Dependabot configuration @@ -415,8 +436,7 @@ void TestAdoptionByExistingLibrary(AbsolutePath repositoryDirectory) .DependsOn(PrepareTemplateReadmes) .Executes(() => { - string[] names = ["Normal", "SourceOnly", "NormalOss", "SourceOnlyOss", "NormalAzdo", "SourceOnlyAzdo"]; - foreach (string name in names) + foreach (string name in TemplateVariantNames) { var templateDirectory = ArtifactsDirectory / "templates" / name; @@ -464,6 +484,17 @@ static void AssertDependencyUpdater(AbsolutePath projectDirectory, bool expectDe $"but found dependabot.yml={hasDependabot} and renovate.json={hasRenovate}"); } + static void AssertAzdoReplacements(AbsolutePath projectDirectory, string organization, string project) + { + // Build/Build.cs is present in both the normal and source-only Azdo variants, unlike the main .csproj, + // whose "Package info" property group (which contains the RepositoryUrl) is only rendered for non-source-only. + var buildScriptFile = projectDirectory / "Build" / "Build.cs"; + string content = buildScriptFile.ReadAllText(); + + Assert.True(content.Contains($"{organization}/{project}", StringComparison.Ordinal), + $"Expected {buildScriptFile} to reference organization '{organization}' and project '{project}'"); + } + bool IsPullRequest =>GitHubActions?.IsPullRequest ?? false; bool IsTag => BranchSpec != null && BranchSpec.Contains("refs/tags", StringComparison.OrdinalIgnoreCase); diff --git a/templates/Source/.template.config/template.json b/templates/Source/.template.config/template.json index 76c5002..8de599d 100644 --- a/templates/Source/.template.config/template.json +++ b/templates/Source/.template.config/template.json @@ -62,7 +62,7 @@ "replaces": "MyOrganization", "isRequired": true }, - "project": { + "projectName": { "type": "parameter", "dataType": "string", "defaultValue": "MyProject",