From 0a344590c19bc9b4e763402179f2d4dadc38f7be Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 3 Sep 2026 23:35:04 +0100 Subject: [PATCH 1/3] build: adopt shared Purview.Build pipeline --- .github/workflows/pr.yml | 73 ++----- .github/workflows/release.yml | 54 +---- .gitignore | 2 + Justfile | 34 +-- build/Directory.Build.props | 12 -- build/Directory.Build.targets | 3 - build/Directory.Packages.props | 15 -- build/Pipeline.slnx | 11 - build/PipelineCLI/GlobalUsings.cs | 11 - build/PipelineCLI/Helpers/DotNetCLIOptions.cs | 9 - build/PipelineCLI/Helpers/PathHelpers.cs | 21 -- build/PipelineCLI/Helpers/TestHelpers.cs | 39 ---- build/PipelineCLI/Modules/BuildModule.cs | 30 --- .../Modules/CreateGitHubReleaseModule.cs | 54 ----- build/PipelineCLI/Modules/LintModule.cs | 45 ---- build/PipelineCLI/Modules/PackModule.cs | 55 ----- .../Modules/PublishLocalNuGetModule.cs | 195 ------------------ .../PipelineCLI/Modules/PublishNuGetModule.cs | 70 ------- build/PipelineCLI/Modules/RestoreModule.cs | 25 --- build/PipelineCLI/Modules/RunTestsModule.cs | 117 ----------- build/PipelineCLI/Modules/VersionModule.cs | 36 ---- build/PipelineCLI/PipelineCLI.csproj | 16 -- build/PipelineCLI/PipelineProjectDirectory.cs | 55 ----- build/PipelineCLI/Program.cs | 42 ---- .../Properties/launchSettings.json | 11 - build/PipelineCLI/Settings/BuildSettings.cs | 34 --- build/PipelineCLI/Settings/GitHubSettings.cs | 32 --- build/PipelineCLI/Settings/NuGetSettings.cs | 22 -- .../Settings/PublishLocalNuGetSettings.cs | 82 -------- build/PipelineCLI/Settings/ReleaseSettings.cs | 19 -- build/PipelineCLI/appsettings.json | 12 -- purview-build.json | 11 + 32 files changed, 53 insertions(+), 1194 deletions(-) delete mode 100644 build/Directory.Build.props delete mode 100644 build/Directory.Build.targets delete mode 100644 build/Directory.Packages.props delete mode 100644 build/Pipeline.slnx delete mode 100644 build/PipelineCLI/GlobalUsings.cs delete mode 100644 build/PipelineCLI/Helpers/DotNetCLIOptions.cs delete mode 100644 build/PipelineCLI/Helpers/PathHelpers.cs delete mode 100644 build/PipelineCLI/Helpers/TestHelpers.cs delete mode 100644 build/PipelineCLI/Modules/BuildModule.cs delete mode 100644 build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs delete mode 100644 build/PipelineCLI/Modules/LintModule.cs delete mode 100644 build/PipelineCLI/Modules/PackModule.cs delete mode 100644 build/PipelineCLI/Modules/PublishLocalNuGetModule.cs delete mode 100644 build/PipelineCLI/Modules/PublishNuGetModule.cs delete mode 100644 build/PipelineCLI/Modules/RestoreModule.cs delete mode 100644 build/PipelineCLI/Modules/RunTestsModule.cs delete mode 100644 build/PipelineCLI/Modules/VersionModule.cs delete mode 100644 build/PipelineCLI/PipelineCLI.csproj delete mode 100644 build/PipelineCLI/PipelineProjectDirectory.cs delete mode 100644 build/PipelineCLI/Program.cs delete mode 100644 build/PipelineCLI/Properties/launchSettings.json delete mode 100644 build/PipelineCLI/Settings/BuildSettings.cs delete mode 100644 build/PipelineCLI/Settings/GitHubSettings.cs delete mode 100644 build/PipelineCLI/Settings/NuGetSettings.cs delete mode 100644 build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs delete mode 100644 build/PipelineCLI/Settings/ReleaseSettings.cs delete mode 100644 build/PipelineCLI/appsettings.json create mode 100644 purview-build.json diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d89252c..5262a2a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -8,44 +8,17 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -env: - DOTNET_NOLOGO: true - jobs: build: name: Build, lint & package - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - fetch-tags: true - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - dotnet-version: "10.0.x" - - - name: Cache NuGet packages - uses: actions/cache@v4 - with: - path: | - ~/.nuget/packages - ~/.local/share/NuGet - key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj', '**/*.props', '**/*.targets', 'global.json') }} - restore-keys: | - ${{ runner.os }}-nuget- - - - name: Run PR pipeline (without tests) - env: - Build__RunTests: "false" - run: dotnet run --project build/PipelineCLI/PipelineCLI.csproj --configuration Release + uses: purview-dev/build/.github/workflows/purview-build.yml@main + with: + build-version: "0.2.1" + run-tests: false + secrets: inherit tests: name: Tests (${{ matrix.project }}) - runs-on: ubuntu-latest - timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -53,31 +26,11 @@ jobs: - Analyzers.UnitTests.csproj - Analyzers.IntegrationTests.csproj - DotNetProjectSdk.IntegrationTests.csproj - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - fetch-tags: true - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - dotnet-version: "10.0.x" - - - name: Cache NuGet packages - uses: actions/cache@v4 - with: - path: | - ~/.nuget/packages - ~/.local/share/NuGet - key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj', '**/*.props', '**/*.targets', 'global.json') }} - restore-keys: | - ${{ runner.os }}-nuget- - - - name: Run tests for ${{ matrix.project }} - env: - Build__RunTests: "true" - Build__TestProjects: ${{ matrix.project }} - Build__RunLint: "false" - Build__RunPack: "false" - run: dotnet run --project build/PipelineCLI/PipelineCLI.csproj --configuration Release + uses: purview-dev/build/.github/workflows/purview-build.yml@main + with: + build-version: "0.2.1" + run-lint: false + run-pack: false + validate-pack: false + test-projects: ${{ matrix.project }} + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9825177..97abb5f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,51 +11,9 @@ concurrency: jobs: release: name: Release packages - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: write - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - fetch-tags: true - - - name: Setup .NET - uses: actions/setup-dotnet@v6 - with: - dotnet-version: "10.0.x" - - - name: Cache NuGet packages - uses: actions/cache@v4 - with: - path: | - ~/.nuget/packages - ~/.local/share/NuGet - key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props', '**/*.csproj', '**/*.props', '**/*.targets', 'global.json') }} - restore-keys: | - ${{ runner.os }}-nuget- - - - name: Check for version bump - id: version - shell: bash - run: | - VERSION=$(node -p "require('./package.json').version") - TAG="v$VERSION" - if git rev-parse "$TAG" >/dev/null 2>&1; then - echo "Version $VERSION is already tagged as $TAG. Skipping release." - echo "should_publish=false" >> "$GITHUB_OUTPUT" - else - echo "New version $VERSION detected. Releasing $TAG." - echo "should_publish=true" >> "$GITHUB_OUTPUT" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - fi - - - name: Run release pipeline - if: steps.version.outputs.should_publish == 'true' - env: - Release__Mode: NuGet - GITHUB_TOKEN: ${{ github.token }} - NuGet__ApiKey: ${{ secrets.NUGET__APIKEY }} - run: dotnet run --project build/PipelineCLI/PipelineCLI.csproj --configuration Release + uses: purview-dev/build/.github/workflows/purview-release.yml@main + with: + build-version: "0.2.1" + release-mode: NuGet + release-branch: main + secrets: inherit \ No newline at end of file diff --git a/.gitignore b/.gitignore index 76ebe1f..3f1a8f3 100644 --- a/.gitignore +++ b/.gitignore @@ -661,3 +661,5 @@ sketch !build/ + +.tools/ diff --git a/Justfile b/Justfile index c5d8cce..06a71b7 100644 --- a/Justfile +++ b/Justfile @@ -4,8 +4,9 @@ solution_file := "src/DotNetProjectSdk.slnx" build_configuration := "Release" artifacts_folder := "./artifacts" -pipeline_solution := "build/Pipeline.slnx" -pipeline_project := "build/PipelineCLI/PipelineCLI.csproj" +pipeline_version := "0.2.1" +pipeline_feed := "https://api.nuget.org/v3/index.json" +pipeline_tool := ".tools/purview-build/purview-build" current_version := `node -p "require('./package.json').version"` @@ -13,49 +14,56 @@ current_version := `node -p "require('./package.json').version"` default: just --list +# Install the shared Purview.Build tool (authenticated to the Purview-Dev feed) if not present +[private] +ensure-pipeline-tool: + if [ ! -x "{{ pipeline_tool }}" ]; then \ + dotnet tool install Purview.Build --tool-path .tools/purview-build --add-source "{{ pipeline_feed }}" --version "{{ pipeline_version }}"; \ + fi + # Run the PR pipeline (restore, build, lint, tests) [group('Pipeline')] pipeline-pr *args: + just ensure-pipeline-tool echo "Running PR pipeline..." - dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} {{ args }} + "{{ pipeline_tool }}" {{ args }} # Run the build pipeline (restore, build, lint) [group('Pipeline')] pipeline-build *args: + just ensure-pipeline-tool echo "Running build pipeline..." - dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} -- --Build:RunTests=false --Release:Mode=None {{ args }} + "{{ pipeline_tool }}" --Build:RunTests=false --Release:Mode=None {{ args }} # Run the release pipeline (restore, build, lint, tests, pack, publish, GitHub release) [group('Pipeline')] pipeline-release *args: + just ensure-pipeline-tool echo "Running release pipeline..." - dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} -- --Release:Mode=NuGet {{ args }} + "{{ pipeline_tool }}" --Release:Mode=NuGet {{ args }} # Run the release pipeline (restore, build, lint, tests, pack, local nuget publish) # Note: `just` runs recipes through the shell, which strips backslashes from unquoted arguments. -# Always use forward slashes for the feed path, e.g. +# Use the LOCAL_NUGET_FEED_PATH environment variable or forward slashes, e.g. # just pipeline-local-release --PublishLocalNuGet:LocalFeedPath=p:/_sync-projects/.local-nuget/ [group('Pipeline')] pipeline-local-release *args: + just ensure-pipeline-tool echo "Running local release pipeline..." - dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} -- --Release:Mode=LocalNuGet {{ args }} + "{{ pipeline_tool }}" --Release:Mode=LocalNuGet {{ args }} # Run the pipeline with tests enabled [group('Pipeline')] pipeline-tests *args: + just ensure-pipeline-tool echo "Running tests pipeline..." - dotnet run --project {{ pipeline_project }} --configuration {{ build_configuration }} -- --Build:RunTests=true --Release:Mode=None {{ args }} + "{{ pipeline_tool }}" --Build:RunTests=true --Release:Mode=None {{ args }} # Open the solution in Visual Studio/ Registered application [group('Utilities')] vs: open {{ solution_file }} -# Open the solution in Visual Studio/ Registered application -[group('Utilities')] -vs-pipeline: - open {{ pipeline_solution }} - # Build the solution for the specified configuration (default: Release) [group('Build and Test')] build *args: diff --git a/build/Directory.Build.props b/build/Directory.Build.props deleted file mode 100644 index c0da58b..0000000 --- a/build/Directory.Build.props +++ /dev/null @@ -1,12 +0,0 @@ - - - Purview.Aspire.ResourceKit - true - - - - - - $(NoWarn);CA1062;CA1515;CA2007;CA1873; - - diff --git a/build/Directory.Build.targets b/build/Directory.Build.targets deleted file mode 100644 index 6d9a3e0..0000000 --- a/build/Directory.Build.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/build/Directory.Packages.props b/build/Directory.Packages.props deleted file mode 100644 index 4741f63..0000000 --- a/build/Directory.Packages.props +++ /dev/null @@ -1,15 +0,0 @@ - - - - [3.2.8,) - - - - - - - - - - - diff --git a/build/Pipeline.slnx b/build/Pipeline.slnx deleted file mode 100644 index 5c5b98f..0000000 --- a/build/Pipeline.slnx +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/build/PipelineCLI/GlobalUsings.cs b/build/PipelineCLI/GlobalUsings.cs deleted file mode 100644 index 0835bee..0000000 --- a/build/PipelineCLI/GlobalUsings.cs +++ /dev/null @@ -1,11 +0,0 @@ -global using Microsoft.Extensions.Configuration; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Logging; -global using Microsoft.Extensions.Options; -global using ModularPipelines; -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; diff --git a/build/PipelineCLI/Helpers/DotNetCLIOptions.cs b/build/PipelineCLI/Helpers/DotNetCLIOptions.cs deleted file mode 100644 index 7f8dc73..0000000 --- a/build/PipelineCLI/Helpers/DotNetCLIOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using ModularPipelines.Options; - -namespace Purview.Aspire.ResourceKit.PipelineCLI.Helpers; - -public sealed record DotNetCLIOptions : CommandLineToolOptions -{ - public static DotNetCLIOptions Create(params string[] commandParts) => - new() { Tool = "dotnet", CommandParts = commandParts }; -} diff --git a/build/PipelineCLI/Helpers/PathHelpers.cs b/build/PipelineCLI/Helpers/PathHelpers.cs deleted file mode 100644 index 36e8ba9..0000000 --- a/build/PipelineCLI/Helpers/PathHelpers.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace Purview.Aspire.ResourceKit.PipelineCLI.Helpers; - -static class PathHelpers -{ - public static string FindRepositoryRoot(string? startDirectory = null) - { - 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"))) - return directory.FullName; - - directory = directory.Parent; - } - - throw new InvalidOperationException("Could not locate the repository root (no package.json found)."); - } -} diff --git a/build/PipelineCLI/Helpers/TestHelpers.cs b/build/PipelineCLI/Helpers/TestHelpers.cs deleted file mode 100644 index c5eaa1a..0000000 --- a/build/PipelineCLI/Helpers/TestHelpers.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Purview.Aspire.ResourceKit.PipelineCLI.Helpers; - -static class TestHelpers -{ - public static string BuildTUnitTreeNodeFilter( - string? assembly = null, - string? @namespace = null, - string? className = null, - string? testNameQuery = null - ) - { - var filter = "/"; - filter += assembly switch - { - null => "*", - _ => assembly, - }; - - filter += @namespace switch - { - null => "*", - _ => @namespace, - }; - - filter += className switch - { - null => "*", - _ => className, - }; - - filter += testNameQuery switch - { - null => "*", - _ => testNameQuery, - }; - - return filter; - } -} diff --git a/build/PipelineCLI/Modules/BuildModule.cs b/build/PipelineCLI/Modules/BuildModule.cs deleted file mode 100644 index fddc45b..0000000 --- a/build/PipelineCLI/Modules/BuildModule.cs +++ /dev/null @@ -1,30 +0,0 @@ -using ModularPipelines.Attributes; -using ModularPipelines.Context; -using ModularPipelines.DotNet.Extensions; -using ModularPipelines.Models; -using ModularPipelines.Modules; - -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; - -[ModuleCategory("Build")] -[DependsOn] -public class BuildModule(IOptions settings) : Module -{ - protected override async Task ExecuteAsync( - IModuleContext context, - CancellationToken cancellationToken - ) - { - return await context - .DotNet() - .Build( - new() - { - ProjectSolution = settings.Value.Solution, - Configuration = settings.Value.Configuration, - NoRestore = true, - }, - cancellationToken: cancellationToken - ); - } -} diff --git a/build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs b/build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs deleted file mode 100644 index 6d24f31..0000000 --- a/build/PipelineCLI/Modules/CreateGitHubReleaseModule.cs +++ /dev/null @@ -1,54 +0,0 @@ -using ModularPipelines.Attributes; -using ModularPipelines.Configuration; -using ModularPipelines.Context; -using ModularPipelines.GitHub.Extensions; -using ModularPipelines.Models; -using ModularPipelines.Modules; - -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; - -[ModuleCategory("Release")] -[DependsOn] -[DependsOn] -public class CreateGitHubReleaseModule(IOptions releaseSettings, IOptions gitSettings) - : Module -{ - protected override ModuleConfiguration Configure() => - ModuleConfiguration - .Create() - .WithSkipWhen(_ => - releaseSettings.Value.Mode is not (ReleaseMode.NuGet or ReleaseMode.GitHubRelease) - || string.IsNullOrWhiteSpace(gitSettings.Value.GetGitHubToken()) - ? SkipDecision.Skip( - "GitHub release creation is disabled. Set Release__Mode=NuGet (or GitHubRelease) and GITHUB_TOKEN to create a GitHub release." - ) - : SkipDecision.DoNotSkip - ) - .Build(); - - protected override async Task ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) - { - var versionResult = await context.GetModule(); - var version = - versionResult.ValueOrDefault - ?? throw new InvalidOperationException("The version was not produced by the version module."); - - var tag = $"v{version}"; - - var repositoryIdString = context.GitHub().EnvironmentVariables.RepositoryId; - if (!long.TryParse(repositoryIdString, out var repositoryId)) - { - throw new InvalidOperationException( - $"Failed to parse RepositoryId '{repositoryIdString}' as a valid long integer." - ); - } - - // Create a new release on GitHub with the specified tag and generate release notes - return await context - .GitHub() - .Client.Repository.Release.Create( - repositoryId, - new NewRelease(tag) { Name = tag, GenerateReleaseNotes = true } - ); - } -} diff --git a/build/PipelineCLI/Modules/LintModule.cs b/build/PipelineCLI/Modules/LintModule.cs deleted file mode 100644 index 4511e1b..0000000 --- a/build/PipelineCLI/Modules/LintModule.cs +++ /dev/null @@ -1,45 +0,0 @@ -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; - -[ModuleCategory("Build")] -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(); - 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 deleted file mode 100644 index 27119b6..0000000 --- a/build/PipelineCLI/Modules/PackModule.cs +++ /dev/null @@ -1,55 +0,0 @@ -using ModularPipelines.Attributes; -using ModularPipelines.Configuration; -using ModularPipelines.Context; -using ModularPipelines.DotNet.Extensions; -using ModularPipelines.DotNet.Options; -using ModularPipelines.Models; -using ModularPipelines.Modules; - -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; - -[ModuleCategory("Build")] -[DependsOn] -[DependsOn] -public sealed class PackModule(IOptions settings, IOptions releaseSettings) - : Module -{ - protected override ModuleConfiguration Configure() => - ModuleConfiguration - .Create() - .WithSkipWhen(_ => - !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(); - - protected override async Task ExecuteAsync( - IModuleContext context, - CancellationToken cancellationToken - ) - { - var versionResult = await context.GetModule(); - var nugetVersion = - versionResult.ValueOrDefault - ?? throw new InvalidOperationException("The version was not produced by the version module."); - - Directory.CreateDirectory(settings.Value.ArtifactsFolder); - - var version = nugetVersion.ToString(); - return await context - .DotNet() - .Pack( - new DotNetPackOptions - { - ProjectSolution = settings.Value.Solution, - Configuration = settings.Value.Configuration, - Output = settings.Value.ArtifactsFolder, - Properties = [("PackageVersion", version), ("Version", version)], - }, - cancellationToken: cancellationToken - ); - } -} diff --git a/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs b/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs deleted file mode 100644 index 9374bc7..0000000 --- a/build/PipelineCLI/Modules/PublishLocalNuGetModule.cs +++ /dev/null @@ -1,195 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using ModularPipelines.Attributes; -using ModularPipelines.Configuration; -using ModularPipelines.Context; -using ModularPipelines.Models; -using ModularPipelines.Modules; -using NuGet.Versioning; - -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; - -[ModuleCategory("Build")] -[DependsOn] -public class PublishLocalNuGetModule( - IOptions localNuGetFeedSettings, - IOptions releaseSettings, - IOptions buildSettings -) : Module -{ - protected override ModuleConfiguration Configure() => - ModuleConfiguration - .Create() - .WithSkipWhen(ctx => - !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(); - - protected override async Task ExecuteAsync( - IModuleContext context, - CancellationToken cancellationToken - ) - { - var localFeedPath = localNuGetFeedSettings.Value.LocalFeedPath; - - var validationResults = new List(); - var validationContext = new ValidationContext(localNuGetFeedSettings.Value); - if ( - !Validator.TryValidateObject( - localNuGetFeedSettings.Value, - validationContext, - validationResults, - validateAllProperties: true - ) - ) - { - foreach (var validationResult in validationResults) - context.Logger.LogError("{Message}", validationResult.ErrorMessage); - - throw new InvalidOperationException( - $"Invalid {nameof(PublishLocalNuGetSettings)} configuration for {nameof(PublishLocalNuGetSettings.LocalFeedPath)}. " - + "Windows paths with backslashes may have been stripped by the shell; " - + "use forward slashes, e.g. --PublishLocalNuGet:LocalFeedPath=p:/_sync-projects/.local-nuget/." - ); - } - - var fullLocalFeedPath = Path.GetFullPath(localFeedPath); - context.Logger.LogInformation("Publishing local NuGet packages to {LocalFeedPath}.", fullLocalFeedPath); - - if (!Directory.Exists(fullLocalFeedPath)) - Directory.CreateDirectory(fullLocalFeedPath); - - var packages = Directory - .GetFiles(buildSettings.Value.ArtifactsFolder, "*.nupkg") - .Concat(Directory.GetFiles(buildSettings.Value.ArtifactsFolder, "*.snupkg")) - .ToArray(); - if (packages.Length == 0) - { - throw new InvalidOperationException( - $"No packages found in {buildSettings.Value.ArtifactsFolder}. The local feed was not populated." - ); - } - - List nupkgPackages = []; - foreach (var package in packages) - { - var fileName = Path.GetFileName(package); - var destinationPath = Path.Combine(fullLocalFeedPath, fileName); - - if (Path.GetExtension(fileName) == ".nupkg") - nupkgPackages.Add(await ParsePackageDetailsAsync(package, cancellationToken)); - - if (!localNuGetFeedSettings.Value.OverwriteExistingPackages && File.Exists(destinationPath)) - { - context.Logger.LogInformation("Package {Package} already exists in local feed. Skipping.", fileName); - File.Delete(package); - - continue; - } - - File.Move(package, destinationPath, true); - context.Logger.LogInformation("Copied package {Package} to local feed.", fileName); - } - - if (localNuGetFeedSettings.Value.ClearPackageCache) - { - context.Logger.LogInformation("Clearing local NuGet package cache..."); - - var globalPackagesResult = await context.Shell.Command.ExecuteCommandLineTool( - DotNetCLIOptions.Create("nuget", "locals", "global-packages", "--list"), - cancellationToken: cancellationToken - ); - if (globalPackagesResult.ExitCode != 0) - return globalPackagesResult; - - var httpCacheResult = await context.Shell.Command.ExecuteCommandLineTool( - DotNetCLIOptions.Create("nuget", "locals", "http-cache", "--list"), - cancellationToken: cancellationToken - ); - if (httpCacheResult.ExitCode != 0) - return httpCacheResult; - - var globalPackagePaths = globalPackagesResult - .StandardOutput.Replace("global-packages: ", "", StringComparison.Ordinal) - .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) - .Where(Directory.Exists); - - var httpCachePaths = httpCacheResult - .StandardOutput.Replace("http-cache: ", "", StringComparison.Ordinal) - .Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries) - .Where(Directory.Exists); - - foreach (var artifact in nupkgPackages) - { -#pragma warning disable CA1308 // Normalize strings to uppercase - var loweredPackageId = artifact.PackageId.ToLowerInvariant(); - var loweredVersion = artifact.Version.ToFullString().ToLowerInvariant(); -#pragma warning restore CA1308 // Normalize strings to uppercase - - foreach (var globalPath in globalPackagePaths) - { - var packagePath = Path.Combine(globalPath, loweredPackageId, artifact.Version.ToFullString()); - if (Directory.Exists(packagePath)) - { - Directory.Delete(packagePath, true); - context.Logger.LogInformation( - "Deleted package {Package} version {Version} from global packages cache.", - artifact.PackageId, - artifact.Version - ); - } - } - foreach (var httpCachePath in httpCachePaths) - { - string[] packagePaths = - [ - Path.Combine(httpCachePath, "list_" + loweredPackageId + ".dat"), - Path.Combine(httpCachePath, "list_" + loweredPackageId + "_index.dat"), - Path.Combine(httpCachePath, "list_" + loweredPackageId + "_range_*.dat"), - Path.Combine(httpCachePath, "nupkg_" + loweredPackageId + "." + loweredVersion + ".dat"), - ]; - - foreach (var path in packagePaths) - { - var directory = Path.GetDirectoryName(path); - var pattern = Path.GetFileName(path); - foreach (var file in Directory.EnumerateFiles(directory!, pattern, SearchOption.AllDirectories)) - { - File.Delete(file); - context.Logger.LogInformation( - "Deleted package {Package} version {Version} from HTTP cache.", - artifact.PackageId, - artifact.Version - ); - } - } - } - } - } - - if (localNuGetFeedSettings.Value.ShutdownDotnetBuilderServer) - { - context.Logger.LogInformation("Shutting down dotnet builder server..."); - - return await context.Shell.Command.ExecuteCommandLineTool( - DotNetCLIOptions.Create("build-server", "shutdown"), - cancellationToken: cancellationToken - ); - } - - return null; - } - - static async Task ParsePackageDetailsAsync(string artifact, CancellationToken cancellationToken) - { - using var packageReader = new NuGet.Packaging.PackageArchiveReader(artifact); - var packaging = await packageReader.GetNuspecReaderAsync(cancellationToken); - - return new(packaging.GetId(), packaging.GetVersion()); - } -} - -record struct PackageDetails(string PackageId, NuGetVersion Version); diff --git a/build/PipelineCLI/Modules/PublishNuGetModule.cs b/build/PipelineCLI/Modules/PublishNuGetModule.cs deleted file mode 100644 index 9c8f74e..0000000 --- a/build/PipelineCLI/Modules/PublishNuGetModule.cs +++ /dev/null @@ -1,70 +0,0 @@ -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; - -[ModuleCategory("Release")] -[DependsOn] -[DependsOn] -public class PublishNuGetModule( - IOptions buildSettings, - IOptions nugetSettings, - IOptions releaseSettings -) : Module -{ - protected override ModuleConfiguration Configure() => - ModuleConfiguration - .Create() - .WithSkipWhen(_ => - releaseSettings.Value.Mode != ReleaseMode.NuGet - || string.IsNullOrWhiteSpace(nugetSettings.Value.GetNuGetAPIKey()) - ? SkipDecision.Skip( - "NuGet publishing is disabled. Set Release__Mode=NuGet and NuGet__ApiKey (or NUGET_APIKEY) to publish packages to nuget.org." - ) - : SkipDecision.DoNotSkip - ) - .Build(); - - protected override async Task ExecuteAsync( - IModuleContext context, - CancellationToken cancellationToken - ) - { - 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) - { - throw new InvalidOperationException($"No NuGet packages found in {buildSettings.Value.ArtifactsFolder}."); - } - - var tasks = packages.Select(package => - context - .DotNet() - .Nuget.Push( - new() - { - Path = package, - Source = nugetSettings.Value.FeedUrl, - ApiKey = nugetSettings.Value.GetNuGetAPIKey(), - SkipDuplicate = true, - }, - cancellationToken: cancellationToken - ) - ); - - return await Task.WhenAll(tasks); - } -} diff --git a/build/PipelineCLI/Modules/RestoreModule.cs b/build/PipelineCLI/Modules/RestoreModule.cs deleted file mode 100644 index 139883c..0000000 --- a/build/PipelineCLI/Modules/RestoreModule.cs +++ /dev/null @@ -1,25 +0,0 @@ -using ModularPipelines.Attributes; -using ModularPipelines.Context; -using ModularPipelines.DotNet.Extensions; -using ModularPipelines.DotNet.Options; -using ModularPipelines.Models; -using ModularPipelines.Modules; - -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; - -[ModuleCategory("Build")] -public class RestoreModule(IOptions settings) : Module -{ - protected override async Task ExecuteAsync( - IModuleContext context, - CancellationToken cancellationToken - ) - { - return await context - .DotNet() - .Restore( - new DotNetRestoreOptions { ProjectSolution = settings.Value.Solution }, - cancellationToken: cancellationToken - ); - } -} diff --git a/build/PipelineCLI/Modules/RunTestsModule.cs b/build/PipelineCLI/Modules/RunTestsModule.cs deleted file mode 100644 index d9c2030..0000000 --- a/build/PipelineCLI/Modules/RunTestsModule.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System.Diagnostics; -using System.Text.RegularExpressions; -using ModularPipelines.Attributes; -using ModularPipelines.Configuration; -using ModularPipelines.Context; -using ModularPipelines.DotNet.Extensions; -using ModularPipelines.DotNet.Options; -using ModularPipelines.Models; -using ModularPipelines.Modules; - -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; - -[ModuleCategory("Build")] -[DependsOn] -public class RunTestsModule(IOptions settings) : Module -{ - protected override ModuleConfiguration Configure() => - ModuleConfiguration - .Create() - .WithSkipWhen(_ => - settings.Value.RunTests - ? SkipDecision.DoNotSkip - : SkipDecision.Skip("Tests are disabled. Set Build__RunTests=true to run them.") - ) - .Build(); - - protected override async Task ExecuteAsync( - IModuleContext context, - CancellationToken cancellationToken - ) - { - 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 matched 'src/tests' (filter: {TestProjects}), despite tests being enabled. Skipping test execution.", - settings.Value.TestProjects - ); - - return []; - } - - 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 - { - Project = project, - Configuration = settings.Value.Configuration, - NoBuild = true, - NoRestore = true, - 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 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 deleted file mode 100644 index 80aa59f..0000000 --- a/build/PipelineCLI/Modules/VersionModule.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Text.Json; -using ModularPipelines.Attributes; -using ModularPipelines.Context; -using ModularPipelines.Modules; -using NuGet.Versioning; - -namespace Purview.Aspire.ResourceKit.PipelineCLI.Modules; - -[ModuleCategory("Build")] -public class VersionModule : Module -{ - protected override async Task ExecuteAsync( - IModuleContext context, - CancellationToken cancellationToken - ) - { - var packageJsonPath = Path.Combine(Environment.CurrentDirectory, "package.json"); - - if (!File.Exists(packageJsonPath)) - throw new FileNotFoundException($"Could not find package.json at {packageJsonPath}"); - - var packageJson = await File.ReadAllTextAsync(packageJsonPath, cancellationToken); - - using var document = JsonDocument.Parse(packageJson); - var version = document.RootElement.GetProperty("version").GetString(); - - if (string.IsNullOrWhiteSpace(version)) - throw new InvalidOperationException("The version field in package.json is missing or empty."); - - if (!NuGetVersion.TryParse(version, out var nugetVersion)) - throw new InvalidOperationException($"The version '{version}' in package.json is not a valid SemVer."); - - context.Summary.KeyValue("Version", "Package version", version); - return nugetVersion; - } -} diff --git a/build/PipelineCLI/PipelineCLI.csproj b/build/PipelineCLI/PipelineCLI.csproj deleted file mode 100644 index 0264da8..0000000 --- a/build/PipelineCLI/PipelineCLI.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - PreserveNewest - - - diff --git a/build/PipelineCLI/PipelineProjectDirectory.cs b/build/PipelineCLI/PipelineProjectDirectory.cs deleted file mode 100644 index 288cda8..0000000 --- a/build/PipelineCLI/PipelineProjectDirectory.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace Purview.Aspire.ResourceKit.PipelineCLI; - -static class PipelineProjectDirectory -{ - const string DirectoryVariable = "MODULAR_PIPELINES_DIRECTORY"; - - public static string Find([CallerFilePath] string sourceFilePath = "") - { - var configuredDirectory = Environment.GetEnvironmentVariable(DirectoryVariable); - if (!string.IsNullOrWhiteSpace(configuredDirectory)) - { - return ValidateConfiguredDirectory(configuredDirectory); - } - - var sourceDirectory = Path.GetDirectoryName(sourceFilePath); - return IsPipelineDirectory(sourceDirectory) ? sourceDirectory! : FindFromBuildOutput(); - } - - static string ValidateConfiguredDirectory(string configuredDirectory) - { - var fullPath = Path.GetFullPath(configuredDirectory); - return IsPipelineDirectory(fullPath) - ? fullPath - : throw new InvalidOperationException( - $"{DirectoryVariable} must point to a directory containing appsettings.json and a project file." - ); - } - - static string FindFromBuildOutput() - { - for ( - var directory = new DirectoryInfo(AppContext.BaseDirectory); - directory is not null; - directory = directory.Parent - ) - { - if (IsPipelineDirectory(directory.FullName)) - { - return directory.FullName; - } - } - - throw new InvalidOperationException( - $"Could not locate the pipeline project directory. Set {DirectoryVariable} to its path." - ); - } - - static bool IsPipelineDirectory(string? directory) => - directory is not null - && Directory.Exists(directory) - && File.Exists(Path.Combine(directory, "appsettings.json")) - && Directory.EnumerateFiles(directory, "*.csproj").Any(); -} diff --git a/build/PipelineCLI/Program.cs b/build/PipelineCLI/Program.cs deleted file mode 100644 index 5d38362..0000000 --- a/build/PipelineCLI/Program.cs +++ /dev/null @@ -1,42 +0,0 @@ -var pipelineDirectory = PipelineProjectDirectory.Find(); -var repositoryRoot = PathHelpers.FindRepositoryRoot(pipelineDirectory); - -var builder = Pipeline.CreateBuilder(args); - -builder - .Configuration.AddJsonFile(Path.Combine(pipelineDirectory, "appsettings.json"), optional: false) - .AddEnvironmentVariables() - .AddCommandLine(args); - -builder.Services.Configure(builder.Configuration.GetSection(BuildSettings.SectionName)); -builder.Services.Configure(builder.Configuration.GetSection(NuGetSettings.SectionName)); -builder.Services.Configure( - builder.Configuration.GetSection(PublishLocalNuGetSettings.SectionName) -); -builder.Services.Configure(builder.Configuration.GetSection(GitHubSettings.SectionName)); -builder.Services.Configure(builder.Configuration.GetSection(ReleaseSettings.SectionName)); - -builder.Services.AddSingleton(serviceProvider => -{ - var settings = serviceProvider.GetRequiredService>(); - var accessToken = settings.Value.GetGitHubToken(); - - return new GitHubClient(new(settings.Value.ProductHeader), new InMemoryCredentialStore(new(accessToken))); -}); - -Environment.CurrentDirectory = repositoryRoot; - -builder - .AddModule() - .AddModule() - .AddModule() - .AddModule() - .AddModule() - .AddModule() - .AddModule() - .AddModule() - .AddModule(); - -await using var pipeline = await builder.BuildAsync(); - -await pipeline.RunAsync(); diff --git a/build/PipelineCLI/Properties/launchSettings.json b/build/PipelineCLI/Properties/launchSettings.json deleted file mode 100644 index d5bbebe..0000000 --- a/build/PipelineCLI/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "Run": { - "commandName": "Project" - }, - "Local-NuGet": { - "commandName": "Project", - "commandLineArgs": "--Release:Mode=LocalNuGet\r\n--PublishLocalNuGet:LocalFeedPath=p:\\_sync-projects\\.local-nuget\\" - } - } -} diff --git a/build/PipelineCLI/Settings/BuildSettings.cs b/build/PipelineCLI/Settings/BuildSettings.cs deleted file mode 100644 index 0a7f159..0000000 --- a/build/PipelineCLI/Settings/BuildSettings.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings; - -public sealed class BuildSettings -{ - public const string SectionName = "Build"; - - public LogLevel LogLevel { get; init; } = LogLevel.Warning; - - [Required(AllowEmptyStrings = false)] - public string Solution { get; init; } = "src/DotNetProjectSdk.slnx"; - - [Required(AllowEmptyStrings = false)] - public string Configuration { get; init; } = "Release"; - - [Required(AllowEmptyStrings = false)] - public string ArtifactsFolder { get; init; } = "artifacts"; - - public bool RunTests { get; init; } = true; - - [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 deleted file mode 100644 index b82ed19..0000000 --- a/build/PipelineCLI/Settings/GitHubSettings.cs +++ /dev/null @@ -1,32 +0,0 @@ -using ModularPipelines.Attributes; - -namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings; - -public sealed record GitHubSettings -{ - public const string SectionName = "GitHub"; - - [SecretValue] - public string? AccessToken { get; init; } - - [SecretValue] - [ConfigurationKeyName("GITHUB_TOKEN")] - public string? EnvAccessToken { get; init; } - - public string ProductHeader { get; init; } = "Purview.SourceGeneratorFramework.Pipeline"; - - 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 deleted file mode 100644 index a2a530b..0000000 --- a/build/PipelineCLI/Settings/NuGetSettings.cs +++ /dev/null @@ -1,22 +0,0 @@ -using ModularPipelines.Attributes; - -namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings; - -public sealed record NuGetSettings -{ - public const string SectionName = "NuGet"; - - [SecretValue] - public string? APIKey { get; set; } - - [SecretValue] - [ConfigurationKeyName("NUGET_APIKEY")] - public string? EnvAPIKey { get; set; } - - public string FeedUrl { get; init; } = "https://api.nuget.org/v3/index.json"; - - public string? GetNuGetAPIKey() => - !string.IsNullOrWhiteSpace(APIKey) ? APIKey - : !string.IsNullOrWhiteSpace(EnvAPIKey) ? EnvAPIKey - : null; -} diff --git a/build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs b/build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs deleted file mode 100644 index 3c5514e..0000000 --- a/build/PipelineCLI/Settings/PublishLocalNuGetSettings.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings; - -public sealed record PublishLocalNuGetSettings : IValidatableObject -{ - public const string SectionName = "PublishLocalNuGet"; - - [Required(AllowEmptyStrings = false)] - public string LocalFeedPath { get; init; } = string.Empty; - - public bool OverwriteExistingPackages { get; init; } = true; - - public bool ShutdownDotnetBuilderServer { get; init; } = true; - - public bool ClearPackageCache { get; init; } = true; - - public IEnumerable Validate(ValidationContext validationContext) - { - if (string.IsNullOrWhiteSpace(LocalFeedPath)) - { - yield return new ValidationResult("LocalFeedPath is required.", [nameof(LocalFeedPath)]); - yield break; - } - - // Path.IsPathRooted("p:foo") returns true, but a drive-relative path like "p:foo" is NOT an - // absolute path: Path.GetFullPath resolves it against the current directory and can silently - // copy packages to an unintended location. This is the classic signature of a Windows path whose - // backslashes were stripped by a sh-style shell, e.g. 'p:\_sync-projects\.local-nuget\'. - if (LocalFeedPath.Length >= 2 && LocalFeedPath[1] == ':') - { - var hasSeparatorAfterDrive = - LocalFeedPath.Length >= 3 - && ( - LocalFeedPath[2] == Path.DirectorySeparatorChar - || LocalFeedPath[2] == Path.AltDirectorySeparatorChar - ); - if (!hasSeparatorAfterDrive) - { - yield return new ValidationResult( - $"LocalFeedPath '{LocalFeedPath}' is drive-relative, not an absolute path. " - + "This is usually caused by the shell stripping backslashes from a Windows path such as " - + $"'p:\\_sync-projects\\.local-nuget\\'. Use forward slashes instead, e.g. " - + "'p:/_sync-projects/.local-nuget/'.", - [nameof(LocalFeedPath)] - ); - yield break; - } - } - - if (!Path.IsPathRooted(LocalFeedPath)) - { - yield return new ValidationResult( - $"LocalFeedPath must be an absolute path. Received: '{LocalFeedPath}'.", - [nameof(LocalFeedPath)] - ); - yield break; - } - - var root = Path.GetPathRoot(LocalFeedPath); - if (string.IsNullOrEmpty(root)) - { - yield return new ValidationResult( - $"LocalFeedPath could not be parsed. Received: '{LocalFeedPath}'.", - [nameof(LocalFeedPath)] - ); - yield break; - } - - var lastChar = root[^1]; - if (lastChar == Path.DirectorySeparatorChar || lastChar == Path.AltDirectorySeparatorChar) - yield break; - - if (root.StartsWith(@"\\", StringComparison.Ordinal) || root.StartsWith("//", StringComparison.Ordinal)) - yield break; - - yield return new ValidationResult( - $"LocalFeedPath must be an absolute path (e.g. 'C:\\folder' or '\\\\server\\share'). Received: '{LocalFeedPath}'.", - [nameof(LocalFeedPath)] - ); - } -} diff --git a/build/PipelineCLI/Settings/ReleaseSettings.cs b/build/PipelineCLI/Settings/ReleaseSettings.cs deleted file mode 100644 index 69fa2a7..0000000 --- a/build/PipelineCLI/Settings/ReleaseSettings.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Purview.Aspire.ResourceKit.PipelineCLI.Settings; - -public enum ReleaseMode -{ - None, - - NuGet, - - GitHubRelease, - - LocalNuGet, -} - -public sealed record ReleaseSettings -{ - public const string SectionName = "Release"; - - public ReleaseMode Mode { get; set; } = ReleaseMode.None; -} diff --git a/build/PipelineCLI/appsettings.json b/build/PipelineCLI/appsettings.json deleted file mode 100644 index 9381c34..0000000 --- a/build/PipelineCLI/appsettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "NuGet": { - "FeedUrl": "https://api.nuget.org/v3/index.json" - }, - "GitHub": { - "AccessToken": null, - "ProductHeader": "Purview.SourceGeneratorFramework.Pipeline" - }, - "Release": { - "Mode": "None" - } -} diff --git a/purview-build.json b/purview-build.json new file mode 100644 index 0000000..42bf1b2 --- /dev/null +++ b/purview-build.json @@ -0,0 +1,11 @@ +{ + "Build": { + "Solution": "src/DotNetProjectSdk.slnx", + "TestRoot": "src/tests", + "TestPatterns": "*Tests.csproj", + "TestFilter": "/*/*/*/*/" + }, + "Release": { + "Mode": "None" + } +} \ No newline at end of file From 65dbb5867678a37e2cb30f2146419462e9be5b8e Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 3 Sep 2026 23:38:31 +0100 Subject: [PATCH 2/3] build: disable symbol-package requirement for pack validation --- purview-build.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/purview-build.json b/purview-build.json index 42bf1b2..3632808 100644 --- a/purview-build.json +++ b/purview-build.json @@ -5,6 +5,9 @@ "TestPatterns": "*Tests.csproj", "TestFilter": "/*/*/*/*/" }, + "PackValidation": { + "RequireSymbolPackage": false + }, "Release": { "Mode": "None" } From 32e5344ef172d2c6b05c02721881e32aa29ec232 Mon Sep 17 00:00:00 2001 From: Kieron Lanning Date: Thu, 3 Sep 2026 23:51:44 +0100 Subject: [PATCH 3/3] chore: bumped version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dcb9d9d..e85fef2 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,4 @@ { "name": "pruview-dotnet-project-sdk", - "version": "1.0.0-prerelease.46" + "version": "1.0.0-prerelease.47" }