diff --git a/build/Directory.Build.props b/build/Directory.Build.props index b614f1c..772d7f7 100644 --- a/build/Directory.Build.props +++ b/build/Directory.Build.props @@ -1,6 +1,6 @@ - Purview.Aspire.ResourceKit + Purview.SourceGeneratorFramework true diff --git a/build/PipelineCLI/GlobalUsings.cs b/build/PipelineCLI/GlobalUsings.cs index 0835bee..ba849f1 100644 --- a/build/PipelineCLI/GlobalUsings.cs +++ b/build/PipelineCLI/GlobalUsings.cs @@ -6,6 +6,6 @@ global using ModularPipelines.Extensions; global using Octokit; global using Octokit.Internal; -global using Purview.Aspire.ResourceKit.PipelineCLI.Helpers; -global using Purview.Aspire.ResourceKit.PipelineCLI.Modules; -global using Purview.Aspire.ResourceKit.PipelineCLI.Settings; +global using Purview.SourceGeneratorFramework.PipelineCLI.Helpers; +global using Purview.SourceGeneratorFramework.PipelineCLI.Modules; +global using Purview.SourceGeneratorFramework.PipelineCLI.Settings; diff --git a/build/PipelineCLI/Helpers/DotNetCLIOptions.cs b/build/PipelineCLI/Helpers/DotNetCLIOptions.cs index 7f8dc73..e967e66 100644 --- a/build/PipelineCLI/Helpers/DotNetCLIOptions.cs +++ b/build/PipelineCLI/Helpers/DotNetCLIOptions.cs @@ -1,6 +1,6 @@ using ModularPipelines.Options; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Helpers; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Helpers; public sealed record DotNetCLIOptions : CommandLineToolOptions { diff --git a/build/PipelineCLI/Helpers/PathHelpers.cs b/build/PipelineCLI/Helpers/PathHelpers.cs index 0c7f972..aed0515 100644 --- a/build/PipelineCLI/Helpers/PathHelpers.cs +++ b/build/PipelineCLI/Helpers/PathHelpers.cs @@ -1,10 +1,13 @@ -namespace Purview.Aspire.ResourceKit.PipelineCLI.Helpers; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Helpers; static class PathHelpers { - public static string FindRepositoryRoot(string startDirectory) + public static string FindRepositoryRoot(string? startDirectory = null) { - var directory = new DirectoryInfo(startDirectory); + if (string.IsNullOrEmpty(startDirectory)) + startDirectory = PipelineProjectDirectory.Find(); + + DirectoryInfo? directory = new(startDirectory); while (directory is not null) { if (File.Exists(Path.Combine(directory.FullName, "package.json"))) diff --git a/build/PipelineCLI/Helpers/TestHelpers.cs b/build/PipelineCLI/Helpers/TestHelpers.cs index c5eaa1a..5fa7e51 100644 --- a/build/PipelineCLI/Helpers/TestHelpers.cs +++ b/build/PipelineCLI/Helpers/TestHelpers.cs @@ -1,4 +1,4 @@ -namespace Purview.Aspire.ResourceKit.PipelineCLI.Helpers; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Helpers; static class TestHelpers { diff --git a/build/PipelineCLI/Modules/BuildModule.cs b/build/PipelineCLI/Modules/BuildModule.cs index fddc45b..6062972 100644 --- a/build/PipelineCLI/Modules/BuildModule.cs +++ b/build/PipelineCLI/Modules/BuildModule.cs @@ -4,7 +4,7 @@ using ModularPipelines.Models; using ModularPipelines.Modules; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules; [ModuleCategory("Build")] [DependsOn] diff --git a/build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs b/build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs index 14de6dd..161894b 100644 --- a/build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs +++ b/build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs @@ -5,7 +5,7 @@ using ModularPipelines.Models; using ModularPipelines.Modules; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules; [ModuleCategory("Release")] [DependsOn] @@ -17,16 +17,10 @@ protected override ModuleConfiguration Configure() => ModuleConfiguration .Create() .WithSkipWhen(_ => - releaseSettings.Value.Mode is ReleaseMode.NuGet or ReleaseMode.GitHubRelease - ? SkipDecision.DoNotSkip - : SkipDecision.Skip( - "Release publishing is disabled. Set Release__Mode=GitHubRelease or Release__Mode=NuGet to create a GitHub release." - ) - ) - .WithSkipWhen(_ => - string.IsNullOrWhiteSpace(gitSettings.Value.GetGitHubToken()) + releaseSettings.Value.Mode is not (ReleaseMode.NuGet or ReleaseMode.GitHubRelease) + || string.IsNullOrWhiteSpace(gitSettings.Value.GetGitHubToken()) ? SkipDecision.Skip( - "GitHub access token is not configured. Set GitHub__AccessToken or GITHUB_TOKEN to create a GitHub release." + "GitHub release creation is disabled. Set Release__Mode=NuGet (or GitHubRelease) and GITHUB_TOKEN to create a GitHub release." ) : SkipDecision.DoNotSkip ) diff --git a/build/PipelineCLI/Modules/LintModule.cs b/build/PipelineCLI/Modules/LintModule.cs index 77d30e8..91ff181 100644 --- a/build/PipelineCLI/Modules/LintModule.cs +++ b/build/PipelineCLI/Modules/LintModule.cs @@ -1,27 +1,44 @@ using ModularPipelines.Attributes; +using ModularPipelines.Configuration; using ModularPipelines.Context; using ModularPipelines.DotNet.Extensions; using ModularPipelines.Models; using ModularPipelines.Modules; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules; [ModuleCategory("Build")] -public sealed class LintModule : Module +public sealed class LintModule(IOptions settings) : Module { + protected override ModuleConfiguration Configure() => + ModuleConfiguration + .Create() + .WithSkipWhen(_ => + settings.Value.RunLint + ? SkipDecision.DoNotSkip + : SkipDecision.Skip("Linting is disabled. Set Build__RunLint=true to enable it.") + ) + .Build(); + protected override async Task ExecuteAsync( IModuleContext context, CancellationToken cancellationToken ) { + var repositoryRoot = PathHelpers.FindRepositoryRoot(); var dotnet = context.DotNet(); - await dotnet.Tool.Restore(new() { Interactive = false }, new(), cancellationToken); - - var pipelineDirectory = PipelineProjectDirectory.Find(); - var repositoryRoot = PathHelpers.FindRepositoryRoot(pipelineDirectory); + var restoreResult = await dotnet.Tool.Restore( + new() { Interactive = false, ToolManifest = Path.Combine(repositoryRoot, ".config", "dotnet-tools.json") }, + new() { WorkingDirectory = repositoryRoot }, + cancellationToken + ); + if (restoreResult.ExitCode != 0) + return restoreResult; + // Restore worked, now run the linter return await context.Shell.Command.ExecuteCommandLineTool( DotNetCLIOptions.Create("tool", "run", "csharpier", "check", repositoryRoot), + new() { WorkingDirectory = repositoryRoot }, cancellationToken: cancellationToken ); } diff --git a/build/PipelineCLI/Modules/PackModule.cs b/build/PipelineCLI/Modules/PackModule.cs index af4b875..1959808 100644 --- a/build/PipelineCLI/Modules/PackModule.cs +++ b/build/PipelineCLI/Modules/PackModule.cs @@ -6,7 +6,7 @@ using ModularPipelines.Models; using ModularPipelines.Modules; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules; [ModuleCategory("Build")] [DependsOn] @@ -18,11 +18,11 @@ protected override ModuleConfiguration Configure() => ModuleConfiguration .Create() .WithSkipWhen(_ => - releaseSettings.Value.Mode != ReleaseMode.None - ? SkipDecision.DoNotSkip - : SkipDecision.Skip( - "Packing is disabled. Set Release__Mode to something other than None to enable it." + !settings.Value.RunPack || releaseSettings.Value.Mode == ReleaseMode.None + ? SkipDecision.Skip( + "Packing is disabled. Set Build__RunPack=true and Release__Mode to something other than None to enable it." ) + : SkipDecision.DoNotSkip ) .Build(); diff --git a/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs b/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs index 1eddee2..ccf35b2 100644 --- a/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs +++ b/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs @@ -6,7 +6,7 @@ using ModularPipelines.Modules; using NuGet.Versioning; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules; [ModuleCategory("Build")] [DependsOn] @@ -20,16 +20,11 @@ protected override ModuleConfiguration Configure() => ModuleConfiguration .Create() .WithSkipWhen(ctx => - ctx.IsRunningLocally() - ? SkipDecision.DoNotSkip - : SkipDecision.Skip("Local NuGet Feed publishing is disabled. This module can only be run locally.") - ) - .WithSkipWhen(_ => - releaseSettings.Value.Mode == ReleaseMode.LocalNuGet - ? SkipDecision.DoNotSkip - : SkipDecision.Skip( - "Local NuGet Feed publishing is disabled. Set Release__Mode=LocalNuGet to enable it." + !ctx.IsRunningLocally() || releaseSettings.Value.Mode != ReleaseMode.LocalNuGet + ? SkipDecision.Skip( + "Local NuGet Feed publishing is disabled. Run the pipeline locally with Release__Mode=LocalNuGet to enable it." ) + : SkipDecision.DoNotSkip ) .Build(); diff --git a/build/PipelineCLI/Modules/PublishNuGetModule.cs b/build/PipelineCLI/Modules/PublishNuGetModule.cs index ae68a4c..d450448 100644 --- a/build/PipelineCLI/Modules/PublishNuGetModule.cs +++ b/build/PipelineCLI/Modules/PublishNuGetModule.cs @@ -5,7 +5,7 @@ using ModularPipelines.Models; using ModularPipelines.Modules; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules; [ModuleCategory("Release")] [DependsOn] @@ -20,16 +20,10 @@ protected override ModuleConfiguration Configure() => ModuleConfiguration .Create() .WithSkipWhen(_ => - releaseSettings.Value.Mode == ReleaseMode.NuGet - ? SkipDecision.DoNotSkip - : SkipDecision.Skip( - "Release publishing is disabled. Set Release__Mode=NuGet to publish packages to nuget.org." - ) - ) - .WithSkipWhen(_ => - string.IsNullOrWhiteSpace(nugetSettings.Value.GetNuGetAPIKey()) + releaseSettings.Value.Mode != ReleaseMode.NuGet + || string.IsNullOrWhiteSpace(nugetSettings.Value.GetNuGetAPIKey()) ? SkipDecision.Skip( - "NuGet API key is not set. Set NuGet__APIKey or NUGET_APIKEY to publish packages." + "NuGet publishing is disabled. Set Release__Mode=NuGet and NuGet__ApiKey (or NUGET_APIKEY) to publish packages to nuget.org." ) : SkipDecision.DoNotSkip ) @@ -40,9 +34,16 @@ protected override ModuleConfiguration Configure() => CancellationToken cancellationToken ) { - var packages = Directory - .EnumerateFiles(buildSettings.Value.ArtifactsFolder, "*.nupkg", SearchOption.TopDirectoryOnly) - .ToList(); + var artifactsFolder = buildSettings.Value.ArtifactsFolder; + if (!Directory.Exists(artifactsFolder)) + { + throw new InvalidOperationException( + $"The artifacts folder '{artifactsFolder}' does not exist. " + + "Ensure the pack step ran (Release__Mode must not be None) before publishing." + ); + } + + var packages = Directory.EnumerateFiles(artifactsFolder, "*.nupkg", SearchOption.TopDirectoryOnly).ToList(); if (packages.Count == 0) { diff --git a/build/PipelineCLI/Modules/RestoreModule.cs b/build/PipelineCLI/Modules/RestoreModule.cs index 139883c..eff5547 100644 --- a/build/PipelineCLI/Modules/RestoreModule.cs +++ b/build/PipelineCLI/Modules/RestoreModule.cs @@ -5,7 +5,7 @@ using ModularPipelines.Models; using ModularPipelines.Modules; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules; [ModuleCategory("Build")] public class RestoreModule(IOptions settings) : Module diff --git a/build/PipelineCLI/Modules/RunTestsModule.cs b/build/PipelineCLI/Modules/RunTestsModule.cs index 556a261..e30dece 100644 --- a/build/PipelineCLI/Modules/RunTestsModule.cs +++ b/build/PipelineCLI/Modules/RunTestsModule.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; +using System.Text.RegularExpressions; using ModularPipelines.Attributes; using ModularPipelines.Configuration; using ModularPipelines.Context; @@ -6,7 +8,7 @@ using ModularPipelines.Models; using ModularPipelines.Modules; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules; [ModuleCategory("Build")] [DependsOn] @@ -27,18 +29,26 @@ protected override ModuleConfiguration Configure() => CancellationToken cancellationToken ) { - var testProjects = Directory.EnumerateFiles("src/tests", "*Tests.csproj", SearchOption.AllDirectories).ToList(); + var testProjects = FilterTestProjects( + Directory.EnumerateFiles("src/tests", "*Tests.csproj", SearchOption.AllDirectories).ToList(), + settings.Value.TestProjects + ); if (testProjects.Count == 0) { context.Logger.LogWarning( - "No test projects found in 'src/tests', despite tests being enabled. Skipping test execution." + "No test projects matched 'src/tests' (filter: {TestProjects}), despite tests being enabled. Skipping test execution.", + settings.Value.TestProjects ); return []; } - var tasks = testProjects.Select(project => - context + var timings = new List<(string Project, TimeSpan Elapsed, int ExitCode)>(); + + var tasks = testProjects.Select(async project => + { + var stopwatch = Stopwatch.StartNew(); + var result = await context .DotNet() .Test( new DotNetTestOptions @@ -50,9 +60,58 @@ CancellationToken cancellationToken Arguments = ["--ignore-exit-code", "8", "--treenode-filter", settings.Value.TestFilter], }, cancellationToken: cancellationToken - ) + ); + stopwatch.Stop(); + + lock (timings) + timings.Add((project, stopwatch.Elapsed, result.ExitCode)); + + return result; + }); + + var results = await Task.WhenAll(tasks); + + context.Logger.LogInformation( + "Test run timings:{NewLine}{Timings}", + Environment.NewLine, + string.Join( + Environment.NewLine, + timings + .OrderByDescending(t => t.Elapsed) + .Select(t => $" {Path.GetFileName(t.Project)}: {t.Elapsed.TotalSeconds:F1}s (exit {t.ExitCode})") + ) ); - return await Task.WhenAll(tasks); + return results; + } + + static IReadOnlyList FilterTestProjects(IReadOnlyList projects, string filter) + { + if (string.IsNullOrWhiteSpace(filter) || filter.Trim() == "*") + return projects; + + var patterns = filter + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(ToRegexPattern) + .ToArray(); + + return projects + .Where(project => + { + var fileName = Path.GetFileName(project); + return patterns.Any(pattern => Regex.IsMatch(fileName, pattern, RegexOptions.IgnoreCase)); + }) + .ToList(); + } + + static string ToRegexPattern(string entry) + { + if (entry.Contains('*', StringComparison.Ordinal)) + { + var escaped = Regex.Escape(entry); + return "^" + escaped.Replace("\\*", ".*", StringComparison.Ordinal) + "$"; + } + + return "^" + Regex.Escape(entry) + "$"; } } diff --git a/build/PipelineCLI/Modules/VersionModule.cs b/build/PipelineCLI/Modules/VersionModule.cs index 80aa59f..83d3631 100644 --- a/build/PipelineCLI/Modules/VersionModule.cs +++ b/build/PipelineCLI/Modules/VersionModule.cs @@ -4,7 +4,7 @@ using ModularPipelines.Modules; using NuGet.Versioning; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Modules; [ModuleCategory("Build")] public class VersionModule : Module diff --git a/build/PipelineCLI/PipelineProjectDirectory.cs b/build/PipelineCLI/PipelineProjectDirectory.cs index 288cda8..b89c24e 100644 --- a/build/PipelineCLI/PipelineProjectDirectory.cs +++ b/build/PipelineCLI/PipelineProjectDirectory.cs @@ -1,6 +1,6 @@ using System.Runtime.CompilerServices; -namespace Purview.Aspire.ResourceKit.PipelineCLI; +namespace Purview.SourceGeneratorFramework.PipelineCLI; static class PipelineProjectDirectory { diff --git a/build/PipelineCLI/Settings/BuildSettings.cs b/build/PipelineCLI/Settings/BuildSettings.cs index 7b91954..100dc71 100644 --- a/build/PipelineCLI/Settings/BuildSettings.cs +++ b/build/PipelineCLI/Settings/BuildSettings.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Settings; public sealed class BuildSettings { @@ -21,4 +21,14 @@ public sealed class BuildSettings [Required(AllowEmptyStrings = false)] public string TestFilter { get; init; } = "/*/*/*/*/"; + + /// + /// Comma-separated list of test project file names (or glob patterns) to run. + /// Empty or "*" runs every test project under src/tests. + /// + public string TestProjects { get; init; } = "*"; + + public bool RunLint { get; init; } = true; + + public bool RunPack { get; init; } = true; } diff --git a/build/PipelineCLI/Settings/GitHubSettings.cs b/build/PipelineCLI/Settings/GitHubSettings.cs index d54f201..0c1baa1 100644 --- a/build/PipelineCLI/Settings/GitHubSettings.cs +++ b/build/PipelineCLI/Settings/GitHubSettings.cs @@ -1,6 +1,6 @@ using ModularPipelines.Attributes; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Settings; public sealed record GitHubSettings { @@ -15,8 +15,18 @@ public sealed record GitHubSettings public string ProductHeader { get; init; } = "Purview.SourceGeneratorFramework.Pipeline"; - public string? GetGitHubToken() => - !string.IsNullOrWhiteSpace(AccessToken) ? AccessToken - : !string.IsNullOrWhiteSpace(EnvAccessToken) ? EnvAccessToken - : null; + public string? GetGitHubToken() + { + if (!string.IsNullOrWhiteSpace(AccessToken)) + return AccessToken; + + if (!string.IsNullOrWhiteSpace(EnvAccessToken)) + return EnvAccessToken; + + // GitHub Actions provisions the automatic GITHUB_TOKEN as a plain environment variable. + // The config binder keys it under the "GitHub" section (GitHub:GITHUB_TOKEN), which the + // standard GITHUB_TOKEN env var does not map to, so read it directly as a fallback. + var processToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); + return string.IsNullOrWhiteSpace(processToken) ? null : processToken; + } } diff --git a/build/PipelineCLI/Settings/NuGetSettings.cs b/build/PipelineCLI/Settings/NuGetSettings.cs index a2a530b..1ccac86 100644 --- a/build/PipelineCLI/Settings/NuGetSettings.cs +++ b/build/PipelineCLI/Settings/NuGetSettings.cs @@ -1,6 +1,6 @@ using ModularPipelines.Attributes; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Settings; public sealed record NuGetSettings { diff --git a/build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs b/build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs index 3c5514e..4d0256f 100644 --- a/build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs +++ b/build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Settings; public sealed record PublishLocalNuGetSettings : IValidatableObject { diff --git a/build/PipelineCLI/Settings/ReleaseSettings.cs b/build/PipelineCLI/Settings/ReleaseSettings.cs index 69fa2a7..7cf2acc 100644 --- a/build/PipelineCLI/Settings/ReleaseSettings.cs +++ b/build/PipelineCLI/Settings/ReleaseSettings.cs @@ -1,4 +1,4 @@ -namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings; +namespace Purview.SourceGeneratorFramework.PipelineCLI.Settings; public enum ReleaseMode { diff --git a/build/PipelineCLI/appsettings.json b/build/PipelineCLI/appsettings.json index 9381c34..52aabe1 100644 --- a/build/PipelineCLI/appsettings.json +++ b/build/PipelineCLI/appsettings.json @@ -1,12 +1,28 @@ { + "Build": { + "Solution": "src/SourceGeneratorFramework.slnx", + "Configuration": "Release", + "ArtifactsFolder": "artifacts", + "RunTests": true, + "TestFilter": "/*/*/*/*/", + "TestProjects": "*", + "RunLint": true, + "RunPack": true + }, "NuGet": { "FeedUrl": "https://api.nuget.org/v3/index.json" }, + "PublishLocalNuGet": { + "LocalFeedPath": "", + "OverwriteExistingPackages": true, + "ShutdownDotnetBuilderServer": true, + "ClearPackageCache": true + }, "GitHub": { "AccessToken": null, "ProductHeader": "Purview.SourceGeneratorFramework.Pipeline" }, - "Release": { - "Mode": "None" - } + "Release": { + "Mode": "None" + } } diff --git a/global.json b/global.json index df658f9..6cef581 100644 --- a/global.json +++ b/global.json @@ -5,7 +5,7 @@ "allowPrerelease": false }, "msbuild-sdks": { - "Purview.DotNetProjectSdk": "1.0.0-prerelease.43" + "Purview.DotNetProjectSdk": "1.0.0-prerelease.44" }, "test": { "runner": "Microsoft.Testing.Platform"