diff --git a/.devops/workflows/Test_Devops_Build.yml b/.devops/workflows/Test_Devops_Build.yml index 343b5e7d..d88bdde5 100644 --- a/.devops/workflows/Test_Devops_Build.yml +++ b/.devops/workflows/Test_Devops_Build.yml @@ -4,6 +4,7 @@ trigger: branches: include: [ main ] pr: + autoCancel: true branches: include: [ main ] variables: @@ -225,6 +226,12 @@ jobs: artifactName: Invex.Atom.Module.DevopsWorkflows.Tests-$(job-runs-on)-$(test-framework) targetPath: $(Build.BinariesDirectory)/Invex.Atom.Module.DevopsWorkflows.Tests + - task: PublishPipelineArtifact@1 + displayName: Invex.Atom.Module.Dotnet.Tests + inputs: + artifactName: Invex.Atom.Module.Dotnet.Tests-$(job-runs-on)-$(test-framework) + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Module.Dotnet.Tests + - task: PublishPipelineArtifact@1 displayName: Invex.Atom.Module.GithubWorkflows.Tests inputs: diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml index 92da2946..cd5bd19c 100644 --- a/.github/workflows/Build.yml +++ b/.github/workflows/Build.yml @@ -3,12 +3,14 @@ name: Build on: push: branches: [ main, feature/**, patch/** ] - release: - types: [ released ] workflow_dispatch: permissions: { } +concurrency: + group: '${{ github.workflow }}-${{ github.ref }}' + cancel-in-progress: true + jobs: SetupBuildInfo: @@ -202,6 +204,12 @@ jobs: name: 'Invex.Atom.Module.DevopsWorkflows.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }}' path: '${{ github.workspace }}/.github/publish/Invex.Atom.Module.DevopsWorkflows.Tests' + - name: Store Invex.Atom.Module.Dotnet.Tests + uses: actions/upload-artifact@v7 + with: + name: 'Invex.Atom.Module.Dotnet.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }}' + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Module.Dotnet.Tests' + - name: Store Invex.Atom.Module.GithubWorkflows.Tests uses: actions/upload-artifact@v7 with: @@ -244,1004 +252,3 @@ jobs: name: GeneratedDocs path: '${{ github.workspace }}/.github/publish/GeneratedDocs' - PublishDocs: - permissions: - contents: write - needs: [ SetupBuildInfo, BuildDocs ] - if: contains(needs.SetupBuildInfo.outputs.build-version, '-') == false - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup .NET 10.0.x - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.0.x - - - name: Retrieve GeneratedDocs - uses: actions/download-artifact@v8 - with: - name: GeneratedDocs - path: '${{ github.workspace }}/.github/artifacts/GeneratedDocs' - - - name: PublishDocs - id: PublishDocs - run: dotnet run --project _atom/_atom.csproj -- PublishDocs --skip --headless - env: - github-token: ${{ secrets.GITHUB_TOKEN }} - - PushToNuget: - needs: [ TestProjects, PackProjects, PackTool, SetupBuildInfo ] - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup .NET 10.0.x - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.0.x - - - name: Retrieve Invex.Atom.Build - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build' - - - name: Retrieve Invex.Atom.Workflows - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows' - - - name: Retrieve Invex.Atom.Module.AzureKeyVault - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.AzureKeyVault - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.AzureKeyVault' - - - name: Retrieve Invex.Atom.Module.AzureStorage - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.AzureStorage - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.AzureStorage' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows' - - - name: Retrieve Invex.Atom.Module.Dotnet - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.Dotnet - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet' - - - name: Retrieve Invex.Atom.Module.GitVersion - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GitVersion - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GitVersion' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows' - - - name: Retrieve Invex.Atom.Tool - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool-windows-latest - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' - - - name: Retrieve Invex.Atom.Tool - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool-windows-11-arm - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' - - - name: Retrieve Invex.Atom.Tool - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool-ubuntu-latest - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' - - - name: Retrieve Invex.Atom.Tool - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool-ubuntu-24.04-arm - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' - - - name: Retrieve Invex.Atom.Tool - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool-macos-15-intel - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' - - - name: Retrieve Invex.Atom.Tool - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool-macos-latest - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' - - - name: PushToNuget - id: PushToNuget - run: dotnet run --project _atom/_atom.csproj -- PushToNuget --skip --headless - env: - build-id: ${{ needs.SetupBuildInfo.outputs.build-id }} - nuget-push-api-key: ${{ secrets.NUGET_PUSH_API_KEY }} - - PushToRelease: - permissions: - contents: write - needs: [ PackProjects, PackTool, TestProjects, SetupBuildInfo ] - if: contains(needs.SetupBuildInfo.outputs.build-version, '-') == false - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup .NET 10.0.x - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.0.x - - - name: Retrieve Invex.Atom.Build - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build' - - - name: Retrieve Invex.Atom.Workflows - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows' - - - name: Retrieve Invex.Atom.Module.AzureKeyVault - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.AzureKeyVault - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.AzureKeyVault' - - - name: Retrieve Invex.Atom.Module.AzureStorage - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.AzureStorage - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.AzureStorage' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows' - - - name: Retrieve Invex.Atom.Module.Dotnet - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.Dotnet - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet' - - - name: Retrieve Invex.Atom.Module.GitVersion - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GitVersion - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GitVersion' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows' - - - name: Retrieve Invex.Atom.Tool - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool-windows-latest - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' - - - name: Retrieve Invex.Atom.Tool - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool-windows-11-arm - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' - - - name: Retrieve Invex.Atom.Tool - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool-ubuntu-latest - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' - - - name: Retrieve Invex.Atom.Tool - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool-ubuntu-24.04-arm - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' - - - name: Retrieve Invex.Atom.Tool - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool-macos-15-intel - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' - - - name: Retrieve Invex.Atom.Tool - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool-macos-latest - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-windows-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-windows-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-windows-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-windows-11-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-windows-11-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-windows-11-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-ubuntu-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-ubuntu-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-ubuntu-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-ubuntu-24.04-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-ubuntu-24.04-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-ubuntu-24.04-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-macos-15-intel-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-macos-15-intel-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-macos-15-intel-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-macos-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-macos-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Tests-macos-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-windows-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-windows-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-windows-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-windows-11-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-windows-11-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-windows-11-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-ubuntu-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-ubuntu-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-ubuntu-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-ubuntu-24.04-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-ubuntu-24.04-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-ubuntu-24.04-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-macos-15-intel-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-macos-15-intel-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-macos-15-intel-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-macos-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-macos-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.Analyzers.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.Analyzers.Tests-macos-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-windows-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-windows-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-windows-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-windows-11-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-windows-11-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-windows-11-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-ubuntu-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-ubuntu-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-ubuntu-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-ubuntu-24.04-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-ubuntu-24.04-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-ubuntu-24.04-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-macos-15-intel-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-macos-15-intel-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-macos-15-intel-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-macos-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-macos-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Build.SourceGenerators.Tests-macos-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-windows-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-windows-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-windows-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-windows-11-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-windows-11-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-windows-11-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-ubuntu-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-ubuntu-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-ubuntu-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-ubuntu-24.04-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-ubuntu-24.04-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-ubuntu-24.04-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-macos-15-intel-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-macos-15-intel-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-macos-15-intel-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-macos-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-macos-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.DevopsWorkflows.Tests-macos-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-windows-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-windows-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-windows-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-windows-11-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-windows-11-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-windows-11-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-ubuntu-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-ubuntu-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-ubuntu-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-ubuntu-24.04-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-ubuntu-24.04-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-ubuntu-24.04-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-macos-15-intel-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-macos-15-intel-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-macos-15-intel-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-macos-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-macos-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Module.GithubWorkflows.Tests-macos-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-windows-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-windows-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-windows-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-windows-11-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-windows-11-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-windows-11-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-ubuntu-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-ubuntu-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-ubuntu-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-ubuntu-24.04-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-ubuntu-24.04-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-ubuntu-24.04-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-macos-15-intel-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-macos-15-intel-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-macos-15-intel-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-macos-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-macos-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Workflows.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Workflows.Tests-macos-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-windows-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-windows-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-windows-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-windows-11-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-windows-11-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-windows-11-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-ubuntu-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-ubuntu-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-ubuntu-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-ubuntu-24.04-arm-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-ubuntu-24.04-arm-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-ubuntu-24.04-arm-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-macos-15-intel-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-macos-15-intel-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-macos-15-intel-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-macos-latest-net8.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-macos-latest-net9.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: Retrieve Invex.Atom.Tool.Tests - uses: actions/download-artifact@v8 - with: - name: Invex.Atom.Tool.Tests-macos-latest-net10.0 - path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' - - - name: PushToRelease - id: PushToRelease - run: dotnet run --project _atom/_atom.csproj -- PushToRelease --skip --headless - env: - build-version: ${{ needs.SetupBuildInfo.outputs.build-version }} - github-token: ${{ secrets.GITHUB_TOKEN }} - diff --git a/.github/workflows/CreateRelease.yml b/.github/workflows/CreateRelease.yml index 20a8d3f5..d7d4a405 100644 --- a/.github/workflows/CreateRelease.yml +++ b/.github/workflows/CreateRelease.yml @@ -30,11 +30,229 @@ jobs: id: SetupBuildInfo run: dotnet run --project _atom/_atom.csproj -- SetupBuildInfo --skip --headless - CreateGithubRelease: + PackProjects: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: PackProjects + id: PackProjects + run: dotnet run --project _atom/_atom.csproj -- PackProjects --skip --headless + + - name: Store Invex.Atom.Build + uses: actions/upload-artifact@v7 + with: + name: Invex.Atom.Build + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Build' + + - name: Store Invex.Atom.Workflows + uses: actions/upload-artifact@v7 + with: + name: Invex.Atom.Workflows + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Workflows' + + - name: Store Invex.Atom.Module.AzureKeyVault + uses: actions/upload-artifact@v7 + with: + name: Invex.Atom.Module.AzureKeyVault + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Module.AzureKeyVault' + + - name: Store Invex.Atom.Module.AzureStorage + uses: actions/upload-artifact@v7 + with: + name: Invex.Atom.Module.AzureStorage + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Module.AzureStorage' + + - name: Store Invex.Atom.Module.DevopsWorkflows + uses: actions/upload-artifact@v7 + with: + name: Invex.Atom.Module.DevopsWorkflows + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Module.DevopsWorkflows' + + - name: Store Invex.Atom.Module.Dotnet + uses: actions/upload-artifact@v7 + with: + name: Invex.Atom.Module.Dotnet + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Module.Dotnet' + + - name: Store Invex.Atom.Module.GitVersion + uses: actions/upload-artifact@v7 + with: + name: Invex.Atom.Module.GitVersion + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Module.GitVersion' + + - name: Store Invex.Atom.Module.GithubWorkflows + uses: actions/upload-artifact@v7 + with: + name: Invex.Atom.Module.GithubWorkflows + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Module.GithubWorkflows' + + PackTool: + runs-on: ${{ matrix.job-runs-on }} + strategy: + matrix: + job-runs-on: + - windows-latest + - windows-11-arm + - ubuntu-latest + - ubuntu-24.04-arm + - macos-15-intel + - macos-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: PackTool + id: PackTool + run: dotnet run --project _atom/_atom.csproj -- PackTool --skip --headless + env: + job-runs-on: ${{ matrix.job-runs-on }} + build-slice: ${{ matrix.job-runs-on }} + + - name: Store Invex.Atom.Tool + uses: actions/upload-artifact@v7 + with: + name: 'Invex.Atom.Tool-${{ matrix.job-runs-on }}' + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Tool' + + TestProjects: + runs-on: ${{ matrix.job-runs-on }} + strategy: + matrix: + job-runs-on: + - windows-latest + - windows-11-arm + - ubuntu-latest + - ubuntu-24.04-arm + - macos-15-intel + - macos-latest + test-framework: [ net8.0, net9.0, net10.0 ] + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Setup .NET 8.0.x + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 8.0.x + + - name: Setup .NET 9.0.x + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 9.0.x + + - name: TestProjects + id: TestProjects + run: dotnet run --project _atom/_atom.csproj -- TestProjects --skip --headless + env: + job-runs-on: ${{ matrix.job-runs-on }} + test-framework: ${{ matrix.test-framework }} + build-slice: '${{ matrix.job-runs-on }}-${{ matrix.test-framework }}' + + - name: Store Invex.Atom.Build.Tests + uses: actions/upload-artifact@v7 + with: + name: 'Invex.Atom.Build.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }}' + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Build.Tests' + + - name: Store Invex.Atom.Build.Analyzers.Tests + uses: actions/upload-artifact@v7 + with: + name: 'Invex.Atom.Build.Analyzers.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }}' + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Build.Analyzers.Tests' + + - name: Store Invex.Atom.Build.SourceGenerators.Tests + uses: actions/upload-artifact@v7 + with: + name: 'Invex.Atom.Build.SourceGenerators.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }}' + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Store Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/upload-artifact@v7 + with: + name: 'Invex.Atom.Module.DevopsWorkflows.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }}' + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Store Invex.Atom.Module.Dotnet.Tests + uses: actions/upload-artifact@v7 + with: + name: 'Invex.Atom.Module.Dotnet.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }}' + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Module.Dotnet.Tests' + + - name: Store Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/upload-artifact@v7 + with: + name: 'Invex.Atom.Module.GithubWorkflows.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }}' + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Store Invex.Atom.Workflows.Tests + uses: actions/upload-artifact@v7 + with: + name: 'Invex.Atom.Workflows.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }}' + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Workflows.Tests' + + - name: Store Invex.Atom.Tool.Tests + uses: actions/upload-artifact@v7 + with: + name: 'Invex.Atom.Tool.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }}' + path: '${{ github.workspace }}/.github/publish/Invex.Atom.Tool.Tests' + + BuildDocs: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: BuildDocs + id: BuildDocs + run: dotnet run --project _atom/_atom.csproj -- BuildDocs --skip --headless + + - name: Store GeneratedDocs + uses: actions/upload-artifact@v7 + with: + name: GeneratedDocs + path: '${{ github.workspace }}/.github/publish/GeneratedDocs' + + DeployRelease: permissions: contents: write - needs: [ SetupBuildInfo ] + needs: [ PackProjects, PackTool, TestProjects, SetupBuildInfo ] + if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest + environment: production steps: - name: Checkout @@ -47,10 +265,990 @@ jobs: with: dotnet-version: 10.0.x - - name: CreateGithubRelease - id: CreateGithubRelease - run: dotnet run --project _atom/_atom.csproj -- CreateGithubRelease --skip --headless + - name: Retrieve Invex.Atom.Build + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build' + + - name: Retrieve Invex.Atom.Workflows + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows' + + - name: Retrieve Invex.Atom.Module.AzureKeyVault + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.AzureKeyVault + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.AzureKeyVault' + + - name: Retrieve Invex.Atom.Module.AzureStorage + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.AzureStorage + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.AzureStorage' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows' + + - name: Retrieve Invex.Atom.Module.Dotnet + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet' + + - name: Retrieve Invex.Atom.Module.GitVersion + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GitVersion + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GitVersion' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows' + + - name: Retrieve Invex.Atom.Tool + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool-windows-latest + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' + + - name: Retrieve Invex.Atom.Tool + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool-windows-11-arm + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' + + - name: Retrieve Invex.Atom.Tool + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool-ubuntu-latest + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' + + - name: Retrieve Invex.Atom.Tool + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool-ubuntu-24.04-arm + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' + + - name: Retrieve Invex.Atom.Tool + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool-macos-15-intel + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' + + - name: Retrieve Invex.Atom.Tool + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool-macos-latest + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-windows-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-windows-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-windows-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-windows-11-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-windows-11-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-windows-11-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-ubuntu-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-ubuntu-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-ubuntu-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-ubuntu-24.04-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-ubuntu-24.04-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-ubuntu-24.04-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-macos-15-intel-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-macos-15-intel-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-macos-15-intel-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-macos-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-macos-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Tests-macos-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-windows-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-windows-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-windows-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-windows-11-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-windows-11-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-windows-11-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-ubuntu-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-ubuntu-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-ubuntu-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-ubuntu-24.04-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-ubuntu-24.04-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-ubuntu-24.04-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-macos-15-intel-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-macos-15-intel-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-macos-15-intel-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-macos-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-macos-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.Analyzers.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.Analyzers.Tests-macos-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.Analyzers.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-windows-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-windows-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-windows-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-windows-11-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-windows-11-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-windows-11-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-ubuntu-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-ubuntu-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-ubuntu-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-ubuntu-24.04-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-ubuntu-24.04-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-ubuntu-24.04-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-macos-15-intel-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-macos-15-intel-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-macos-15-intel-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-macos-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-macos-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Build.SourceGenerators.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Build.SourceGenerators.Tests-macos-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Build.SourceGenerators.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-windows-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-windows-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-windows-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-windows-11-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-windows-11-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-windows-11-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-ubuntu-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-ubuntu-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-ubuntu-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-ubuntu-24.04-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-ubuntu-24.04-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-ubuntu-24.04-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-macos-15-intel-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-macos-15-intel-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-macos-15-intel-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-macos-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-macos-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.DevopsWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.DevopsWorkflows.Tests-macos-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.DevopsWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-windows-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-windows-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-windows-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-windows-11-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-windows-11-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-windows-11-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-ubuntu-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-ubuntu-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-ubuntu-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-ubuntu-24.04-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-ubuntu-24.04-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-ubuntu-24.04-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-macos-15-intel-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-macos-15-intel-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-macos-15-intel-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-macos-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-macos-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.Dotnet.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.Dotnet.Tests-macos-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.Dotnet.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-windows-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-windows-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-windows-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-windows-11-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-windows-11-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-windows-11-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-ubuntu-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-ubuntu-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-ubuntu-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-ubuntu-24.04-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-ubuntu-24.04-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-ubuntu-24.04-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-macos-15-intel-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-macos-15-intel-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-macos-15-intel-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-macos-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-macos-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Module.GithubWorkflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Module.GithubWorkflows.Tests-macos-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Module.GithubWorkflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-windows-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-windows-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-windows-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-windows-11-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-windows-11-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-windows-11-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-ubuntu-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-ubuntu-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-ubuntu-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-ubuntu-24.04-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-ubuntu-24.04-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-ubuntu-24.04-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-macos-15-intel-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-macos-15-intel-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-macos-15-intel-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-macos-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-macos-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Workflows.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Workflows.Tests-macos-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Workflows.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-windows-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-windows-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-windows-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-windows-11-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-windows-11-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-windows-11-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-ubuntu-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-ubuntu-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-ubuntu-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-ubuntu-24.04-arm-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-ubuntu-24.04-arm-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-ubuntu-24.04-arm-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-macos-15-intel-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-macos-15-intel-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-macos-15-intel-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-macos-latest-net8.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-macos-latest-net9.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: Retrieve Invex.Atom.Tool.Tests + uses: actions/download-artifact@v8 + with: + name: Invex.Atom.Tool.Tests-macos-latest-net10.0 + path: '${{ github.workspace }}/.github/artifacts/Invex.Atom.Tool.Tests' + + - name: DeployRelease + id: DeployRelease + run: dotnet run --project _atom/_atom.csproj -- DeployRelease --skip --headless env: build-version: ${{ needs.SetupBuildInfo.outputs.build-version }} github-token: ${{ secrets.GITHUB_TOKEN }} + nuget-push-api-key: ${{ secrets.NUGET_PUSH_API_KEY }} + + PublishReleaseDocs: + permissions: + contents: write + needs: [ DeployRelease, BuildDocs ] + if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + environment: production + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Retrieve GeneratedDocs + uses: actions/download-artifact@v8 + with: + name: GeneratedDocs + path: '${{ github.workspace }}/.github/artifacts/GeneratedDocs' + + - name: PublishReleaseDocs + id: PublishReleaseDocs + run: dotnet run --project _atom/_atom.csproj -- PublishReleaseDocs --skip --headless + env: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/Dependabot Enable auto-merge.yml b/.github/workflows/Dependabot Enable auto-merge.yml index e933209a..bfa99268 100644 --- a/.github/workflows/Dependabot Enable auto-merge.yml +++ b/.github/workflows/Dependabot Enable auto-merge.yml @@ -6,6 +6,10 @@ on: permissions: { } +concurrency: + group: '${{ github.workflow }}-${{ github.ref }}' + cancel-in-progress: true + jobs: ApproveDependabotPr: diff --git a/.github/workflows/Validate.yml b/.github/workflows/Validate.yml index 2116fa8c..b760b50e 100644 --- a/.github/workflows/Validate.yml +++ b/.github/workflows/Validate.yml @@ -7,6 +7,10 @@ on: permissions: { } +concurrency: + group: '${{ github.workflow }}-${{ github.ref }}' + cancel-in-progress: true + jobs: SetupBuildInfo: @@ -142,10 +146,8 @@ jobs: CheckPrForBreakingChanges: permissions: - checks: write - contents: write - id-token: write - pull-requests: write + contents: read + pull-requests: read needs: [ SetupBuildInfo ] if: github.event_name == 'pull_request' runs-on: ubuntu-latest @@ -166,10 +168,12 @@ jobs: run: dotnet run --project _atom/_atom.csproj -- CheckPrForBreakingChanges --skip --headless env: build-version: ${{ needs.SetupBuildInfo.outputs.build-version }} - github-token: ${{ secrets.GITHUB_TOKEN }} - pull-request-number: ${{ github.event.number }} WaitForCopilotReview: + permissions: + contents: read + pull-requests: read + if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: diff --git a/Invex.Atom.slnx b/Invex.Atom.slnx index 19d1cb16..a729de31 100644 --- a/Invex.Atom.slnx +++ b/Invex.Atom.slnx @@ -33,6 +33,7 @@ + diff --git a/README.md b/README.md index 9909b646..c106640c 100644 --- a/README.md +++ b/README.md @@ -494,7 +494,7 @@ internal interface IBuild : IBuildDefinition - [Overview](docs/modules/overview.md) — What a module is and how to add one - [.NET](docs/modules/dotnet.md) — `Invex.Atom.Module.Dotnet` - [GitHub Workflows](docs/modules/github-workflows.md) — `Invex.Atom.Module.GithubWorkflows` -- [DevOps Workflows](docs/modules/devops-workflows.md) — `Invex.Atom.Module.DevopsWorkflows` +- [DevOps Workflows](docs/modules/devops-workflows.md) — Azure Pipelines generation, pools, PR controls, and concurrency - [Azure Key Vault](docs/modules/azure-key-vault.md) — `Invex.Atom.Module.AzureKeyVault` - [Azure Storage](docs/modules/azure-storage.md) — `Invex.Atom.Module.AzureStorage` - [GitVersion](docs/modules/git-version.md) — `Invex.Atom.Module.GitVersion` @@ -512,6 +512,7 @@ internal interface IBuild : IBuildDefinition and more - [Source Generators](docs/developer-guide/source-generators.md) — How the Atom analysers and source generators work - [Testing](docs/developer-guide/testing.md) — Using `Invex.Atom.TestUtils` +- [Releasing Atom](docs/developer-guide/releasing-atom.md) — Protected release process and rollback ### Reference diff --git a/_atom/IBuild.cs b/_atom/IBuild.cs index fecb196b..8a082155 100644 --- a/_atom/IBuild.cs +++ b/_atom/IBuild.cs @@ -40,6 +40,12 @@ internal interface IBuild : IWorkflowBuildDefinition, WorkflowLabels.Dotnet.Framework.Net_10_0, ]; + static readonly IBuildOption CancelSupersededWorkflowRuns = BuildOptions.Github.Concurrency.Set( + TextExpressions.Concat([ + TextExpressions.Github.GithubWorkflow.Evaluate(), "-", TextExpressions.Github.GithubRef.Evaluate(), + ]), + true); + IReadOnlyList IBuildDefinition.Options => [ BuildOptions.GitVersion.ProvideBuildId, @@ -101,16 +107,11 @@ internal interface IBuild : IWorkflowBuildDefinition, Options = [ BuildOptions.Target.SuppressArtifactPublishing, - BuildOptions.Inject.Secret(nameof(GithubToken)), BuildOptions.Github.TokenPermissions.Set(new Permissions.Exact(new() { - IdTokens = PermissionsLevel.Write, - Contents = PermissionsLevel.Write, - PullRequests = PermissionsLevel.Write, - Checks = PermissionsLevel.Write, + Contents = PermissionsLevel.Read, + PullRequests = PermissionsLevel.Read, })), - BuildOptions.Inject.Param(nameof(PullRequestNumber), - TextExpressions.Github.GithubEvent["number"]), BuildOptions.Target.RunIfWorkflowCondition( TextExpressions.Github.GithubEventName.EqualToString("pull_request")), ], @@ -120,12 +121,20 @@ internal interface IBuild : IWorkflowBuildDefinition, Options = [ BuildOptions.Inject.Secret(nameof(GithubToken)), + BuildOptions.Github.TokenPermissions.Set(new Permissions.Exact(new() + { + Contents = PermissionsLevel.Read, + PullRequests = PermissionsLevel.Read, + })), BuildOptions.Inject.Param(nameof(PullRequestNumber), TextExpressions.Github.GithubEvent["number"]), + BuildOptions.Target.RunIfWorkflowCondition( + TextExpressions.Github.GithubEventName.EqualToString("pull_request")), ], }, ], Types = [WorkflowTypes.Github.Action], + Options = [CancelSupersededWorkflowRuns], }, new("Build") { @@ -136,7 +145,6 @@ internal interface IBuild : IWorkflowBuildDefinition, { IncludedBranches = ["main", "feature/**", "patch/**"], }, - new GithubTrigger(new On.Release([On.Release.ReleaseType.released])), ], Targets = [ @@ -174,52 +182,67 @@ internal interface IBuild : IWorkflowBuildDefinition, ], }, new(nameof(BuildDocs)), - new(nameof(PublishDocs)) + ], + Types = [WorkflowTypes.Github.Action], + Options = [CancelSupersededWorkflowRuns], + }, + new("CreateRelease") + { + Triggers = [WorkflowTriggers.Manual], + Targets = + [ + new(nameof(SetupBuildInfo)), + new(nameof(PackProjects)), + new(nameof(PackTool)) { - Options = + MatrixDimensions = [ - BuildOptions.Inject.Secret(nameof(GithubToken)), - new GithubTokenPermissionsOption(new Permissions.Exact(new() + new(nameof(JobRunsOn)) { - Contents = PermissionsLevel.Write, - })), - BuildOptions.Target.RunIfWorkflowCondition(TextExpressions - .Target - .ParamOutput(this, nameof(SetupBuildInfo), nameof(BuildVersion)) - .Contains("-") - .EqualTo(false)), + Values = PlatformNames.ToList(), + }, ], + Options = [BuildOptions.Github.RunsOn.SetByMatrix], }, - new(nameof(PushToNuget)) + new(nameof(TestProjects)) { - Options = [BuildOptions.Inject.Secret(nameof(NugetApiKey))], + MatrixDimensions = + [ + new(nameof(JobRunsOn)) + { + Values = PlatformNames.ToList(), + }, + new(nameof(TestFramework)) + { + Values = FrameworkNames.ToList(), + }, + ], + Options = + [ + BuildOptions.Github.RunsOn.SetByMatrix, + BuildOptions.Steps.SetupDotnet.Dotnet80X(), + BuildOptions.Steps.SetupDotnet.Dotnet90X(), + ], }, - new(nameof(PushToRelease)) + new(nameof(BuildDocs)), + new(nameof(DeployRelease)) { Options = [ BuildOptions.Inject.Secret(nameof(GithubToken)), + BuildOptions.Inject.Secret(nameof(NugetApiKey)), new GithubTokenPermissionsOption(new Permissions.Exact(new() { Contents = PermissionsLevel.Write, })), - BuildOptions.Target.RunIfWorkflowCondition(TextExpressions - .Target - .ParamOutput(this, nameof(SetupBuildInfo), nameof(BuildVersion)) - .Contains("-") - .EqualTo(false)), + BuildOptions.Deploy.ToEnvironment("production"), + BuildOptions.Target.RunIfWorkflowCondition( + TextExpressions.Github.GithubEventName.EqualToString("workflow_dispatch")), + BuildOptions.Target.RunIfWorkflowCondition( + TextExpressions.Github.GithubRef.EqualToString("refs/heads/main")), ], }, - ], - Types = [WorkflowTypes.Github.Action], - }, - new("CreateRelease") - { - Triggers = [WorkflowTriggers.Manual], - Targets = - [ - new(nameof(SetupBuildInfo)), - new(nameof(CreateGithubRelease)) + new(nameof(PublishReleaseDocs)) { Options = [ @@ -228,6 +251,11 @@ internal interface IBuild : IWorkflowBuildDefinition, { Contents = PermissionsLevel.Write, })), + BuildOptions.Deploy.ToEnvironment("production"), + BuildOptions.Target.RunIfWorkflowCondition( + TextExpressions.Github.GithubEventName.EqualToString("workflow_dispatch")), + BuildOptions.Target.RunIfWorkflowCondition( + TextExpressions.Github.GithubRef.EqualToString("refs/heads/main")), ], }, ], @@ -279,7 +307,12 @@ internal interface IBuild : IWorkflowBuildDefinition, }, ], Types = [WorkflowTypes.Devops.Pipeline], - Options = [BuildOptions.Inject.Param(nameof(NugetDryRun), true), BuildOptions.Devops.VariableGroup.Atom], + Options = + [ + BuildOptions.Inject.Param(nameof(NugetDryRun), true), + BuildOptions.Devops.VariableGroup.Atom, + BuildOptions.Devops.PullRequest.AutoCancel, + ], }, // Dependabot @@ -344,6 +377,7 @@ internal interface IBuild : IWorkflowBuildDefinition, }, ], Types = [WorkflowTypes.Github.Action], + Options = [CancelSupersededWorkflowRuns], }, ]; } diff --git a/_atom/RepoUtils/ICheckPrForBreakingChanges.cs b/_atom/RepoUtils/ICheckPrForBreakingChanges.cs index 47793205..1d429e7a 100644 --- a/_atom/RepoUtils/ICheckPrForBreakingChanges.cs +++ b/_atom/RepoUtils/ICheckPrForBreakingChanges.cs @@ -2,7 +2,7 @@ namespace Atom.RepoUtils; internal sealed record ReleaseInfo(string CommitHash, SemVer Version); -internal interface ICheckPrForBreakingChanges : IGithubHelper, IPullRequestHelper, ISetupBuildInfo, IApiSurfaceHelper +internal interface ICheckPrForBreakingChanges : ISetupBuildInfo, IApiSurfaceHelper { private RootedPath[] FilesToCheck => RootedFileSystem @@ -13,13 +13,9 @@ internal interface ICheckPrForBreakingChanges : IGithubHelper, IPullRequestHelpe Target CheckPrForBreakingChanges => t => t - .RequiresParam(nameof(GithubToken), nameof(PullRequestNumber)) .ConsumesVariable(nameof(SetupBuildInfo), nameof(BuildVersion)) - .Executes(async cancellationToken => + .Executes(() => { - var owner = Github.Variables.RepositoryOwner; - Logger.LogDebug("Target repository owner: {Owner}", owner); - using var repo = new Repository(RootedFileSystem.AtomRootDirectory); var currentCommitHash = repo.Head.Tip.Sha; @@ -123,17 +119,10 @@ Safe to merge without version bump considerations. _ => false, }; - Logger.LogInformation("Adding check status to pull request with status: {Status}", - hasInvalidChanges - ? "failure" - : "success"); - - await AddCheckStatus(owner, - hasInvalidChanges - ? "failure" - : "success", - body, - cancellationToken); + Logger.LogInformation("{BreakingChangesReport}", body); + + if (hasInvalidChanges) + throw new StepFailedException("Breaking API changes require an appropriate version bump."); }); ReleaseInfo? FindLatestReleaseInfo(Repository repo, SemVer currentVersion) @@ -168,79 +157,4 @@ await AddCheckStatus(owner, return new(version.Tag.Target.Sha, version.Version); } - - private async Task AddCheckStatus( - string owner, - string status, - string description, - CancellationToken cancellationToken) - { - var repository = Github.Variables - .Repository - .Split('/') - .Last(); - - Logger.LogDebug("Target repository: {Repository}", repository); - - var productHeader = new ProductHeaderValue("Atom"); - var connection = new Connection(productHeader, new InMemoryCredentialStore(GithubToken)); - - var repoQuery = new Query() - .Repository(repository, owner) - .Select(r => new - { - r.Id, - }) - .Compile(); - - var repoQueryResult = await connection.Run(repoQuery, cancellationToken: cancellationToken); - - if (repoQueryResult.Id.Value is null) - throw new StepFailedException("Could not find repository."); - - var prQuery = new Query() - .Repository(repository, owner) - .PullRequest(PullRequestNumber) - .Select(p => new - { - p.Id, - p.HeadRefOid, - }) - .Compile(); - - var prQueryResult = await connection.Run(prQuery, cancellationToken: cancellationToken); - - if (prQueryResult.Id.Value is null) - throw new StepFailedException("Could not find pull request."); - - using var repo = new Repository(RootedFileSystem.AtomRootDirectory); - - var checkRunMutation = new Mutation() - .CreateCheckRun(new CreateCheckRunInput - { - RepositoryId = repoQueryResult.Id, - Name = "API Surface Breaking Changes Check", - HeadSha = prQueryResult.HeadRefOid, - Status = RequestableCheckStatusState.Completed, - Conclusion = status == "success" - ? CheckConclusionState.Success - : CheckConclusionState.Failure, - CompletedAt = DateTimeOffset.UtcNow, - Output = new() - { - Title = "Breaking Changes Analysis", - Summary = description, - }, - }) - .Select(x => new - { - x.ClientMutationId, - }) - .Compile(); - - var checkRunResult = await connection.Run(checkRunMutation, cancellationToken: cancellationToken); - - if (checkRunResult is null) - throw new StepFailedException("Could not create check run."); - } } diff --git a/_atom/Targets/IDeployTargets.cs b/_atom/Targets/IDeployTargets.cs index 421cc62c..3c224adc 100644 --- a/_atom/Targets/IDeployTargets.cs +++ b/_atom/Targets/IDeployTargets.cs @@ -2,6 +2,11 @@ internal interface IDeployTargets : INugetHelper, IGithubReleaseHelper, ISetupBuildInfo { + private static IReadOnlyList ReleaseArtifactNames => + ReleaseAssetManifest.CreateArtifactNames(IBuildTargets.ProjectsToPack, + Projects.Invex_Atom_Tool.Name, + ITestTargets.ProjectsToTest); + [ParamDefinition("nuget-push-feed", "The Nuget feed to push to.")] string NugetFeed => GetParam(() => NugetFeed, "https://api.nuget.org/v3/index.json"); @@ -17,23 +22,7 @@ internal interface IDeployTargets : INugetHelper, IGithubReleaseHelper, ISetupBu .ConsumesArtifacts(nameof(IBuildTargets.PackProjects), IBuildTargets.ProjectsToPack) .ConsumesArtifact(nameof(IBuildTargets.PackTool), Projects.Invex_Atom_Tool.Name, PlatformNames) .DependsOn(nameof(ITestTargets.TestProjects)) - .Executes(async cancellationToken => - { - // Push project packages - foreach (var project in IBuildTargets.ProjectsToPack) - await PushProject(project, NugetFeed, NugetApiKey, cancellationToken: cancellationToken); - - // Push Atom tool package - platform-specific + multi-targeted - foreach (var atomToolPackagePath in RootedFileSystem.Directory.GetFiles( - RootedFileSystem.AtomArtifactsDirectory / Projects.Invex_Atom_Tool.Name, - "*.nupkg", - SearchOption.AllDirectories)) - await PushPackageToNuget( - RootedFileSystem.AtomArtifactsDirectory / Projects.Invex_Atom_Tool.Name / atomToolPackagePath, - NugetFeed, - NugetApiKey, - cancellationToken: cancellationToken); - }); + .Executes(PushAllPackagesToNuget); Target PushToNugetDevops => d => d @@ -46,29 +35,131 @@ await PushPackageToNuget( .DependsOn(nameof(ITestTargets.TestProjects)) .Executes(() => Logger.LogInformation("Simulating push to Nuget feed")); - Target PushToRelease => + Target DeployRelease => d => d - .DescribedAs("Pushes the packages to the release feed.") - .RequiresParam(nameof(GithubToken)) + .DescribedAs("Publishes a validated release from a protected main-branch workflow.") + .RequiresParam(nameof(GithubToken), nameof(NugetFeed), nameof(NugetApiKey)) .ConsumesVariable(nameof(SetupBuildInfo), nameof(BuildVersion)) .ConsumesArtifacts(nameof(IBuildTargets.PackProjects), IBuildTargets.ProjectsToPack) .ConsumesArtifact(nameof(IBuildTargets.PackTool), Projects.Invex_Atom_Tool.Name, PlatformNames) .ConsumesArtifacts(nameof(ITestTargets.TestProjects), ITestTargets.ProjectsToTest, PlatformNames.SelectMany(platform => FrameworkNames.Select(framework => $"{platform}-{framework}"))) - .Executes(async () => + .Executes(async cancellationToken => { - foreach (var artifact in IBuildTargets.ProjectsToPack.Concat(ITestTargets.ProjectsToTest)) - await UploadArtifactToRelease(artifact, $"v{BuildVersion}"); + ValidateTrustedReleaseContext(); + ValidateLocalReleaseArtifacts(); + + var tag = $"v{BuildVersion.Major}.{BuildVersion.Minor}.{BuildVersion.Patch}"; + var draft = await CreateRelease(tag, Github.Variables.Sha, tag, draft: true); + + if (draft is null) + { + Logger.LogInformation("Release deployment dry run completed for {Tag}.", tag); + + return; + } + + var nugetPublicationStarted = false; + + try + { + nugetPublicationStarted = true; + await PushAllPackagesToNuget(cancellationToken); + + foreach (var artifactName in ReleaseArtifactNames) + await UploadArtifactToRelease(artifactName, draft); + + var expectedAssets = ReleaseAssetManifest.CreateExpectedAssetNames(ReleaseArtifactNames); + var uploadedAssets = await GetReleaseAssetNames(draft.Id, cancellationToken: cancellationToken); + ReleaseAssetManifest.ValidateUploadedAssets(expectedAssets, uploadedAssets); + + await PublishRelease(draft.Id, cancellationToken: cancellationToken); + } + catch (Exception deploymentException) + { + try + { + await DeleteRelease(draft.Id, cancellationToken: CancellationToken.None); + } + catch (Exception rollbackException) + { + throw new StepFailedException( + "Release deployment and draft cleanup both failed. Inspect the draft release and " + + "deprecate any NuGet packages already published before issuing a new patch version.", + new AggregateException(deploymentException, rollbackException)); + } + + if (deploymentException is OperationCanceledException) + { + if (nugetPublicationStarted) + Logger.LogError( + "Release deployment was cancelled after NuGet publication started. Inspect the " + + "feed and deprecate any packages already published before issuing a new patch version."); + + throw; + } + + throw new StepFailedException( + "Release deployment failed and its draft was deleted. If any NuGet packages were already " + + "published, deprecate that version and issue a new patch; never overwrite it.", + deploymentException); + } }); - Target CreateGithubRelease => - d => d - .DescribedAs("Creates a release on GitHub.") - .RequiresParam(nameof(GithubToken)) - .ConsumesVariable(nameof(SetupBuildInfo), nameof(BuildVersion)) - .Executes(async () => await CreateRelease( - $"v{BuildVersion.Major}.{BuildVersion.Minor}.{BuildVersion.Patch}", - "main", - $"v{BuildVersion.Major}.{BuildVersion.Minor}.{BuildVersion.Patch}")); + private async Task PushAllPackagesToNuget(CancellationToken cancellationToken) + { + foreach (var project in IBuildTargets.ProjectsToPack) + await PushProject(project, NugetFeed, NugetApiKey, cancellationToken: cancellationToken); + + foreach (var atomToolPackagePath in RootedFileSystem.Directory.GetFiles( + RootedFileSystem.AtomArtifactsDirectory / Projects.Invex_Atom_Tool.Name, + "*.nupkg", + SearchOption.AllDirectories)) + await PushPackageToNuget(RootedFileSystem.CreateRootedPath(atomToolPackagePath), + NugetFeed, + NugetApiKey, + cancellationToken: cancellationToken); + } + + private void ValidateTrustedReleaseContext() + { + if (!Github.IsGithubActions) + { + if (!NugetDryRun) + throw new StepFailedException("Release deployment outside GitHub Actions requires --nuget-dry-run."); + + return; + } + + if (!string.Equals(Github.Variables.EventName, "workflow_dispatch", StringComparison.Ordinal) || + !string.Equals(Github.Variables.Ref, "refs/heads/main", StringComparison.Ordinal)) + throw new StepFailedException("Production releases may only be manually dispatched from refs/heads/main."); + } + + private void ValidateLocalReleaseArtifacts() + { + foreach (var artifactName in ReleaseArtifactNames) + { + var artifactPath = RootedFileSystem.AtomArtifactsDirectory / artifactName; + + if (!artifactPath.DirectoryExists) + throw new StepFailedException($"Required release artifact '{artifactName}' is missing."); + + if (RootedFileSystem.Directory.GetFiles(artifactPath, "*", SearchOption.AllDirectories) + .Length is 0) + throw new StepFailedException($"Required release artifact '{artifactName}' is empty."); + } + + var toolPackageCount = RootedFileSystem.Directory.GetFiles( + RootedFileSystem.AtomArtifactsDirectory / Projects.Invex_Atom_Tool.Name, + "*.nupkg", + SearchOption.AllDirectories) + .Length; + + if (toolPackageCount < PlatformNames.Length) + throw new StepFailedException( + $"The tool release artifact contains {toolPackageCount} packages; expected at least " + + $"{PlatformNames.Length}, one for each platform."); + } } diff --git a/_atom/Targets/IDocTargets.cs b/_atom/Targets/IDocTargets.cs index 763b7d0e..209f77da 100644 --- a/_atom/Targets/IDocTargets.cs +++ b/_atom/Targets/IDocTargets.cs @@ -26,4 +26,13 @@ internal interface IDocTargets : IDocFxHelper .DependsOn(nameof(SetupBuildInfo)) .Executes(cancellationToken => PublishDocFxDocsToGithub(GithubToken, GeneratedDocsArtifactName, cancellationToken)); + + Target PublishReleaseDocs => + t => t + .DescribedAs("Publishes validated release documentation to Github Pages.") + .RequiresParam(nameof(GithubToken)) + .ConsumesArtifact(nameof(BuildDocs), GeneratedDocsArtifactName) + .DependsOn(nameof(IDeployTargets.DeployRelease)) + .Executes(cancellationToken => + PublishDocFxDocsToGithub(GithubToken, GeneratedDocsArtifactName, cancellationToken)); } diff --git a/_atom/Targets/ITestTargets.cs b/_atom/Targets/ITestTargets.cs index 25a57f23..b5e33220 100644 --- a/_atom/Targets/ITestTargets.cs +++ b/_atom/Targets/ITestTargets.cs @@ -8,6 +8,7 @@ internal interface ITestTargets : IDotnetTestHelper Projects.Invex_Atom_Build_Analyzers_Tests.Name, Projects.Invex_Atom_Build_SourceGenerators_Tests.Name, Projects.Invex_Atom_Module_DevopsWorkflows_Tests.Name, + Projects.Invex_Atom_Module_Dotnet_Tests.Name, Projects.Invex_Atom_Module_GithubWorkflows_Tests.Name, Projects.Invex_Atom_Workflows_Tests.Name, Projects.Invex_Atom_Tool_Tests.Name, diff --git a/_atom/Targets/ReleaseAssetManifest.cs b/_atom/Targets/ReleaseAssetManifest.cs new file mode 100644 index 00000000..2e68824f --- /dev/null +++ b/_atom/Targets/ReleaseAssetManifest.cs @@ -0,0 +1,44 @@ +namespace Atom.Targets; + +internal static class ReleaseAssetManifest +{ + internal static IReadOnlyList CreateArtifactNames( + IEnumerable packageProjects, + string toolProject, + IEnumerable testProjects) => + packageProjects + .Append(toolProject) + .Concat(testProjects) + .Distinct(StringComparer.Ordinal) + .ToArray(); + + internal static IReadOnlyList CreateExpectedAssetNames(IEnumerable artifactNames) => + artifactNames + .Select(name => $"{name}.zip") + .Order(StringComparer.Ordinal) + .ToArray(); + + internal static void ValidateUploadedAssets( + IEnumerable expectedAssetNames, + IEnumerable uploadedAssetNames) + { + var expected = expectedAssetNames + .Order(StringComparer.Ordinal) + .ToArray(); + + var uploaded = uploadedAssetNames + .Order(StringComparer.Ordinal) + .ToArray(); + + if (uploaded.Length != + uploaded + .Distinct(StringComparer.Ordinal) + .Count()) + throw new StepFailedException("The GitHub release contains duplicate asset names."); + + if (!expected.SequenceEqual(uploaded, StringComparer.Ordinal)) + throw new StepFailedException( + $"GitHub release asset verification failed. Expected: {string.Join(", ", expected)}. " + + $"Uploaded: {string.Join(", ", uploaded)}."); + } +} diff --git a/_atom/_usings.cs b/_atom/_usings.cs index 7102ce14..225d8959 100644 --- a/_atom/_usings.cs +++ b/_atom/_usings.cs @@ -1,4 +1,5 @@ global using System.Runtime.InteropServices; +global using System.Runtime.CompilerServices; global using Atom.RepoUtils; global using Atom.Targets; global using Invex.Atom.Build; @@ -36,3 +37,5 @@ global using Octokit.GraphQL.Model; global using Commit = LibGit2Sharp.Commit; global using Repository = LibGit2Sharp.Repository; + +[assembly: InternalsVisibleTo("Atom.Tests")] diff --git a/docs/developer-guide/releasing-atom.md b/docs/developer-guide/releasing-atom.md new file mode 100644 index 00000000..28e483f3 --- /dev/null +++ b/docs/developer-guide/releasing-atom.md @@ -0,0 +1,45 @@ +# Releasing Atom + +Atom releases use the generated `CreateRelease` GitHub Actions workflow. Continuous `Build` and +`Validate` workflows never receive production publishing credentials. + +## Required repository configuration + +Create a GitHub Environment named `production` and configure: + +1. Required reviewers who are authorised to publish Atom. +2. A deployment branch rule allowing only `main`. +3. The `NUGET_PUSH_API_KEY` environment secret. + +The generated workflow independently requires `workflow_dispatch` on `refs/heads/main`. Environment +protection is still required because reviewer and deployment-branch rules are repository settings, +not properties stored in the workflow file. + +## Release procedure + +1. Confirm the intended release commit is merged to `main` and the validation workflow is green. +2. Open **Actions > CreateRelease > Run workflow**. +3. Select `main`; do not dispatch another branch or tag. +4. Review and approve the `production` deployment. +5. Confirm the workflow builds, tests, packages, creates an exact-SHA draft, pushes NuGet packages, + verifies every expected GitHub release asset, publishes the release, and then publishes docs. + +The deployment job is the only job that receives the NuGet key. It creates a draft release tied to +`GITHUB_SHA` and makes it public only after package pushes and the remote asset manifest succeed. + +## Failure and rollback + +If deployment fails after draft creation, Atom deletes the draft and its uploaded assets +automatically. If draft deletion also fails, delete the draft and tag manually before retrying. + +NuGet publication is immutable and cannot be rolled back transactionally. If one package was +published before a later push failed: + +1. Deprecate every package published with the affected version and explain that the release is + incomplete. +2. Do not overwrite or unlist-and-reuse that version. +3. Fix the failure and publish a new patch version. +4. Delete any remaining draft release and assets for the failed version. + +For a failed permission or ref check, do not bypass the condition. Dispatch the workflow from +`main` and use the protected `production` environment. diff --git a/docs/modules/devops-workflows.md b/docs/modules/devops-workflows.md index fd162639..039a0ad9 100644 --- a/docs/modules/devops-workflows.md +++ b/docs/modules/devops-workflows.md @@ -68,5 +68,30 @@ WorkflowLabels.Devops.Pool.MacOs_Latest - `BuildOptions.Devops.DevopsPool.SetByMatrix` — set the agent pool from a matrix dimension - `BuildOptions.Devops.VariableGroup.*` — reference Azure DevOps variable groups +- `BuildOptions.Devops.Concurrency.RunLatest` — run only the latest pipeline waiting on an exclusive lock +- `BuildOptions.Devops.Concurrency.Sequential` — run every pipeline waiting on an exclusive lock in sequence +- `BuildOptions.Devops.PullRequest.AutoCancel` — cancel an in-progress PR run when a new commit is pushed +- `BuildOptions.Devops.PullRequest.KeepRunning` — allow in-progress PR runs to finish after new commits - `ProvideDevopsRunIdAsWorkflowId` — use the DevOps run ID as the Atom build ID +Configure the lock behavior on a `WorkflowDefinition`: + +```csharp +Options = +[ + BuildOptions.Devops.Concurrency.Sequential, +]; +``` + +Azure Pipelines applies `lockBehavior` to resources protected by an +[exclusive lock check](https://learn.microsoft.com/azure/devops/pipelines/process/approvals#exclusive-lock). + +Configure pull request auto-cancellation alongside a pull request trigger: + +```csharp +new("Validate") +{ + Triggers = [WorkflowTriggers.PullInto("main", "develop")], + Options = [BuildOptions.Devops.PullRequest.AutoCancel], +} +``` diff --git a/docs/modules/github-workflows.md b/docs/modules/github-workflows.md index 7545ca26..93d70674 100644 --- a/docs/modules/github-workflows.md +++ b/docs/modules/github-workflows.md @@ -90,8 +90,25 @@ WorkflowLabels.Github.RunsOn.MacOs_Latest - `BuildOptions.Github.RunsOn.SetByMatrix` — set the runner from a matrix dimension - `GithubTokenPermissionsOption` — fine-grained `GITHUB_TOKEN` permissions +- `BuildOptions.Github.Concurrency.Set(...)` — limit concurrent workflow runs sharing a group - GitHub-specific triggers (e.g. `release` events) +Configure concurrency on a `WorkflowDefinition` with a literal group or a text expression: + +```csharp +Options = +[ + BuildOptions.Github.Concurrency.Set( + TextExpressions.Concat( + [ + TextExpressions.Github.GithubWorkflow.Evaluate(), + "-", + TextExpressions.Github.GithubRef.Evaluate(), + ]), + true), +]; +``` + ### Dependabot Configuration Generate Dependabot configuration alongside your workflow files: @@ -110,4 +127,3 @@ WorkflowPresets.Github.Dependabot(new() ], }) ``` - diff --git a/docs/toc.yml b/docs/toc.yml index 4b16b5c9..460a2e55 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -90,9 +90,10 @@ href: developer-guide/source-generators.md - name: Testing href: developer-guide/testing.md + - name: Releasing Atom + href: developer-guide/releasing-atom.md - name: Reference href: reference/cli.md items: - name: CLI href: reference/cli.md - diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Extensions/DevopsBuildOptionsExtensions.cs b/src/Invex.Atom.Module.DevopsWorkflows/Extensions/DevopsBuildOptionsExtensions.cs index e8653432..0ae0d868 100644 --- a/src/Invex.Atom.Module.DevopsWorkflows/Extensions/DevopsBuildOptionsExtensions.cs +++ b/src/Invex.Atom.Module.DevopsWorkflows/Extensions/DevopsBuildOptionsExtensions.cs @@ -157,6 +157,40 @@ public DevopsCheckoutStep Checkout(DevopsCheckoutStep step) => step; } + /// + /// Creates Azure Pipelines concurrency options. + /// + [PublicAPI] + public sealed class DevopsConcurrencyOptions + { + /// + /// Runs only the latest pipeline waiting on an exclusive lock, cancelling earlier waiting runs. + /// + public DevopsConcurrencyOption RunLatest => field ??= new(TextExpressions.Raw("runLatest")); + + /// + /// Runs every pipeline waiting on an exclusive lock in sequence. + /// + public DevopsConcurrencyOption Sequential => field ??= new(TextExpressions.Raw("sequential")); + } + + /// + /// Creates Azure Pipelines pull request options. + /// + [PublicAPI] + public sealed class DevopsPullRequestOptions + { + /// + /// Cancels an in-progress pull request run when a new commit is pushed to the pull request. + /// + public DevopsPullRequestOption AutoCancel => field ??= new(true); + + /// + /// Allows in-progress pull request runs to continue when new commits are pushed. + /// + public DevopsPullRequestOption KeepRunning => field ??= new(false); + } + [PublicAPI] public sealed class DevopsOptions { @@ -168,6 +202,16 @@ public sealed class DevopsOptions public DevopsStepsOptions Steps => field ??= new(); + /// + /// Gets options that control how pipeline runs wait on exclusive locks. + /// + public DevopsConcurrencyOptions Concurrency => field ??= new(); + + /// + /// Gets options that control pull request pipeline behavior. + /// + public DevopsPullRequestOptions PullRequest => field ??= new(); + public ProvideDevopsRunIdAsWorkflowId ProvideDevopsRunIdAsWorkflowId => field ??= new(); } diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Devops/DevopsWorkflowBuilder.cs b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Devops/DevopsWorkflowBuilder.cs index 09327d1c..ac047264 100644 --- a/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Devops/DevopsWorkflowBuilder.cs +++ b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Devops/DevopsWorkflowBuilder.cs @@ -23,6 +23,8 @@ public DevopsPipeline Build(WorkflowModel workflow) => Pr = BuildPr(workflow), Parameters = BuildParameters(workflow), Variables = BuildVariables(workflow), + LockBehavior = DevopsConcurrencyOption.Get(workflow.Options) + ?.LockBehavior, }; private static Trigger? BuildTrigger(WorkflowModel workflow) @@ -115,6 +117,8 @@ public DevopsPipeline Build(WorkflowModel workflow) => private static Pr? BuildPr(WorkflowModel workflow) { + var pullRequestOption = DevopsPullRequestOption.Get(workflow.Options); + var prTriggers = workflow .Triggers .OfType() @@ -145,7 +149,7 @@ public DevopsPipeline Build(WorkflowModel workflow) => if (!hasBranches && !hasPaths) return null; - if (!hasPaths && includedBranches.Length > 0 && excludedBranches.Length is 0) + if (pullRequestOption is null && !hasPaths && includedBranches.Length > 0 && excludedBranches.Length is 0) return new Pr.BranchList { Branches = new(includedBranches.Select(TextExpressions.Raw)), @@ -175,6 +179,9 @@ public DevopsPipeline Build(WorkflowModel workflow) => : null, } : null, + AutoCancel = pullRequestOption is not null + ? TextExpressions.From(pullRequestOption.AutoCancel) + : null, }; } diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Options/DevopsConcurrencyOption.cs b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Options/DevopsConcurrencyOption.cs new file mode 100644 index 00000000..25d0d496 --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Options/DevopsConcurrencyOption.cs @@ -0,0 +1,10 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Workflows.Options; + +/// +/// Configures how Azure Pipelines handles runs waiting on an exclusive lock. +/// +/// +/// The Azure Pipelines lock behavior. Supported values are runLatest and sequential. +/// +[PublicAPI] +public sealed record DevopsConcurrencyOption(TextExpression LockBehavior) : IBuildOption; diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Options/DevopsPullRequestOption.cs b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Options/DevopsPullRequestOption.cs new file mode 100644 index 00000000..5f0312ca --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Options/DevopsPullRequestOption.cs @@ -0,0 +1,10 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Workflows.Options; + +/// +/// Configures Azure Pipelines pull request behavior. +/// +/// +/// Whether a new commit to a pull request cancels its in-progress pipeline run. +/// +[PublicAPI] +public sealed record DevopsPullRequestOption(bool AutoCancel) : IBuildOption; diff --git a/src/Invex.Atom.Module.Dotnet/Helpers/INugetHelper.cs b/src/Invex.Atom.Module.Dotnet/Helpers/INugetHelper.cs index 5f8e753e..4a123022 100644 --- a/src/Invex.Atom.Module.Dotnet/Helpers/INugetHelper.cs +++ b/src/Invex.Atom.Module.Dotnet/Helpers/INugetHelper.cs @@ -100,8 +100,19 @@ async Task PushProject( /// Optional path to a NuGet configuration file to use instead of the default one. /// A cancellation token to observe while waiting for the task to complete. /// A representing the asynchronous operation. + /// + /// Thrown when the dotnet nuget push process exits with a non-zero exit code. + /// The exception message includes the exit code and the sanitized standard error output; + /// any occurrence of in the stderr is replaced with [REDACTED] + /// to prevent credential exposure in logs and error reports. + /// /// - /// If is true, the push operation will be simulated. + /// If is true, the push operation will be simulated and no + /// process will be started. + /// + /// If is cancelled while the process is running, the underlying + /// propagates to the caller unchanged. + /// /// [PublicAPI] async Task PushPackageToNuget( @@ -126,12 +137,22 @@ async Task PushPackageToNuget( ? $" --configfile \"{configFile}\"" : string.Empty; - var processRunResult = await ProcessRunner.RunAsync(new("dotnet", - $"nuget push \"{packagePath}\"{configFileFlag} --source {feed} --api-key {apiKey}"), + var processRunResult = await ProcessRunner.RunAsync( + new("dotnet", $"nuget push \"{packagePath}\"{configFileFlag} --source {feed} --api-key {apiKey}") + { + AllowFailedResult = true, + }, cancellationToken); if (processRunResult.ExitCode is not 0) - Logger.LogError("Failed to push package to Nuget: {ProcessRunResult}", processRunResult.Error); + { + var sanitizedStderr = apiKey.Length > 0 + ? processRunResult.Error.Replace(apiKey, "[REDACTED]", StringComparison.Ordinal) + : processRunResult.Error; + + throw new StepFailedException( + $"NuGet push failed with exit code {processRunResult.ExitCode}. Stderr: {sanitizedStderr}"); + } Logger.LogInformation("Package pushed"); } diff --git a/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowOptionsExtensions.cs b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowOptionsExtensions.cs index 06271956..8732b800 100644 --- a/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowOptionsExtensions.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowOptionsExtensions.cs @@ -234,6 +234,43 @@ public GithubTokenPermissionsOption Set(Permissions permissions) => new(permissions); } + /// + /// Creates GitHub Actions workflow concurrency options. + /// + [PublicAPI] + public sealed class GithubConcurrencyOptions + { + /// + /// Configures a workflow concurrency group. + /// + /// The concurrency group key. + /// Whether to cancel an in-progress workflow in the same group. + /// The workflow concurrency option. + public GithubConcurrencyOption Set(TextExpression group, TextExpression? cancelInProgress = null) => + new(group, cancelInProgress); + + /// + /// Configures a workflow concurrency group. + /// + /// The concurrency group key. + /// Whether to cancel an in-progress workflow in the same group. + /// The workflow concurrency option. + public GithubConcurrencyOption Set(TextExpression group, bool cancelInProgress) => + new(group, TextExpressions.From(cancelInProgress)); + + /// + /// Configures a workflow concurrency group. + /// + /// The concurrency group key. + /// Whether to cancel an in-progress workflow in the same group. + /// The workflow concurrency option. + public GithubConcurrencyOption Set(string group, bool? cancelInProgress = null) => + new(TextExpressions.Raw(group), + cancelInProgress.HasValue + ? TextExpressions.From(cancelInProgress.Value) + : null); + } + [PublicAPI] public sealed class GithubDependabotOptions { @@ -258,6 +295,8 @@ public sealed class GithubOptions public GithubTokenPermissionsOptions TokenPermissions => field ??= new(); + public GithubConcurrencyOptions Concurrency => field ??= new(); + public GithubDependabotOptions Dependabot => field ??= new(); public GithubStepsOptions Steps => field ??= new(); diff --git a/src/Invex.Atom.Module.GithubWorkflows/GithubActions/GithubWorkflowFileWriter.cs b/src/Invex.Atom.Module.GithubWorkflows/GithubActions/GithubWorkflowFileWriter.cs index 62701ab9..cffc65f3 100644 --- a/src/Invex.Atom.Module.GithubWorkflows/GithubActions/GithubWorkflowFileWriter.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/GithubActions/GithubWorkflowFileWriter.cs @@ -22,7 +22,34 @@ protected override string WriteWorkflow(WorkflowModel workflow) var actionWriter = new GithubActionWriter(); actionWriter.Write(resolvedWorkflow); - return actionWriter.TextWriter.ToString(); + return EnsureConcurrencyGroupKey(actionWriter.TextWriter.ToString(), resolvedWorkflow.Concurrency); + } + + private static string EnsureConcurrencyGroupKey(string workflow, Concurrency? concurrency) + { + if (concurrency is null) + return workflow; + + const string concurrencyHeader = "concurrency:"; + var headerIndex = workflow.IndexOf(concurrencyHeader, StringComparison.Ordinal); + + if (headerIndex < 0) + throw new InvalidOperationException("The GitHub Actions writer did not serialize workflow concurrency."); + + var groupLineIndex = headerIndex + concurrencyHeader.Length; + + while (groupLineIndex < workflow.Length && workflow[groupLineIndex] is '\r' or '\n') + groupLineIndex++; + + const string indentation = " "; + + if (!workflow + .AsSpan(groupLineIndex) + .StartsWith(indentation)) + throw new InvalidOperationException("The GitHub Actions writer serialized an invalid concurrency group."); + + // Invex.StructuredText.GithubActions 1.2 omits the required key while retaining the correctly formatted value. + return workflow.Insert(groupLineIndex + indentation.Length, "group: "); } private GithubAction BuildWorkflow(WorkflowModel workflow) => @@ -36,6 +63,13 @@ private GithubAction BuildWorkflow(WorkflowModel workflow) => .ToList(), Permissions = BuildPermissions(GithubTokenPermissionsOption.Get(workflow.Options) ?? BuildOptions.Github.TokenPermissions.NoneAll), + Concurrency = GithubConcurrencyOption.Get(workflow.Options) is { } concurrency + ? new() + { + Group = concurrency.Group, + CancelInProgress = concurrency.CancelInProgress, + } + : null, }; private List BuildTriggers(WorkflowModel workflow) => diff --git a/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseApi.cs b/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseApi.cs new file mode 100644 index 00000000..05da2183 --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseApi.cs @@ -0,0 +1,57 @@ +namespace Invex.Atom.Module.GithubWorkflows.Helpers; + +/// +/// Defines the low-level GitHub release API operations used by . +/// +/// +/// This interface is the injectable seam between and the Octokit HTTP +/// client, allowing unit tests to verify exact request payloads and transitions without hitting the live +/// GitHub API. +/// The built-in implementation, , delegates directly to an Octokit +/// instance. +/// Register a custom implementation via dependency injection to replace or wrap the default behaviour. +/// +[PublicAPI] +public interface IGithubReleaseApi +{ + /// Creates a new GitHub release. + /// The numeric GitHub repository identifier. + /// The payload describing the release to create. + /// The created . + Task CreateRelease(long repositoryId, NewRelease newRelease); + + /// + /// Finds a GitHub release by its Git tag name, returning null when no matching release exists. + /// + /// The numeric GitHub repository identifier. + /// The Git tag name to search for (e.g. "v1.0.0"). + /// + /// The matching , or null if no release with the given tag is found. + /// + Task FindReleaseByTag(long repositoryId, string tagName); + + /// Updates an existing GitHub release. + /// The numeric GitHub repository identifier. + /// The numeric release identifier. + /// The update payload. + /// The updated . + Task EditRelease(long repositoryId, long releaseId, ReleaseUpdate update); + + /// Returns all assets currently attached to a GitHub release. + /// The numeric GitHub repository identifier. + /// The numeric release identifier. + /// A read-only list of instances. + Task> GetReleaseAssets(long repositoryId, long releaseId); + + /// Permanently deletes a GitHub release. + /// The numeric GitHub repository identifier. + /// The numeric release identifier. + /// A that completes when the release has been deleted. + Task DeleteRelease(long repositoryId, long releaseId); + + /// Uploads a release asset to an existing GitHub release. + /// The target . + /// The upload payload including the asset stream and content-type. + /// The created . + Task UploadAsset(Release release, ReleaseAssetUpload upload); +} diff --git a/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseHelper.cs b/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseHelper.cs index 31eef4c9..3a82d800 100644 --- a/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseHelper.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseHelper.cs @@ -4,12 +4,45 @@ /// Provides helper methods for interacting with GitHub Releases within Invex.Atom builds. /// /// -/// This interface extends to provide functionality for -/// uploading artifacts to a GitHub Release, leveraging the GitHub API. +/// +/// This interface extends to provide functionality for creating, finding, +/// publishing, inspecting, and deleting GitHub Releases, as well as uploading assets. +/// +/// +/// All operations go through the seam. Register a custom +/// in dependency injection to replace the default Octokit +/// client; this is the recommended approach for unit testing. +/// /// [PublicAPI] public interface IGithubReleaseHelper : IGithubHelper { + /// + /// Returns the registered , falling back to an + /// backed by when + /// no registration is present. + /// + private IGithubReleaseApi GetOrCreateReleaseApi() => + Services.GetService() ?? + new OctokitGithubReleaseApi(new(new("Invex.Atom"), new InMemoryCredentialStore(new(GithubToken)))); + + /// + /// Parses the GitHub repository ID from the GITHUB_REPOSITORY_ID environment variable. + /// + /// The parsed numeric repository identifier. + /// + /// Thrown when the environment variable is absent or cannot be parsed as a . + /// + private long ParseRepositoryId() + { + if (!long.TryParse(Github.Variables.RepositoryId, out var repositoryId)) + throw new InvalidOperationException( + $"Unable to parse GitHub repository id from '{Github.VariableNames.RepositoryId}' " + + $"(value: '{Github.Variables.RepositoryId}')."); + + return repositoryId; + } + /// /// Creates a GitHub Release for a new tag, tagging a specific commit or the latest commit on a branch. /// @@ -64,8 +97,6 @@ public interface IGithubReleaseHelper : IGithubHelper return null; } - var client = new GitHubClient(new("Invex.Atom"), new InMemoryCredentialStore(new(GithubToken))); - var newRelease = new NewRelease(tagName) { Name = name ?? tagName, @@ -77,11 +108,164 @@ public interface IGithubReleaseHelper : IGithubHelper if (!string.IsNullOrEmpty(targetCommitish)) newRelease.TargetCommitish = targetCommitish; - if (!long.TryParse(Github.Variables.RepositoryId, out var repositoryId)) - throw new InvalidOperationException( - $"Unable to parse GitHub repository id from '{Github.VariableNames.RepositoryId}' (value: '{Github.Variables.RepositoryId}')."); + return await GetOrCreateReleaseApi() + .CreateRelease(ParseRepositoryId(), newRelease); + } + + /// + /// Finds an existing GitHub Release by its Git tag name. + /// + /// The tag name to look up (e.g. "v1.0.0"). + /// + /// If true (default), the lookup is simulated (logged but not executed) when not running in + /// GitHub Actions, and null is returned. + /// + /// + /// A token that can be used to cancel the operation before it begins. + /// Note: cancellation is checked before the network call; the underlying HTTP request is not cancelled. + /// + /// + /// The matching , null if no release with that tag exists, or null + /// when the operation was simulated. + /// + /// Thrown if the GitHub repository ID cannot be parsed. + /// Thrown if is already cancelled. + [PublicAPI] + async Task FindRelease( + string tagName, + bool dryRunWhenNotRunningInGithubActions = true, + CancellationToken cancellationToken = default) + { + if (!Github.IsGithubActions && dryRunWhenNotRunningInGithubActions) + { + Logger.LogWarning("Not running in GitHub Actions, simulating lookup of release {TagName}.", tagName); + + return null; + } + + cancellationToken.ThrowIfCancellationRequested(); - return await client.Repository.Release.Create(repositoryId, newRelease); + return await GetOrCreateReleaseApi() + .FindReleaseByTag(ParseRepositoryId(), tagName); + } + + /// + /// Publishes a draft GitHub Release, making it publicly visible. + /// + /// The numeric identifier of the draft release to publish. + /// + /// If true (default), the publish operation is simulated (logged but not executed) when not + /// running in GitHub Actions, and null is returned. + /// + /// + /// A token that can be used to cancel the operation before it begins. + /// Note: cancellation is checked before the network call; the underlying HTTP request is not cancelled. + /// + /// + /// The updated with set to false, + /// or null when the operation was simulated. + /// + /// Thrown if the GitHub repository ID cannot be parsed. + /// Thrown if is already cancelled. + [PublicAPI] + async Task PublishRelease( + long releaseId, + bool dryRunWhenNotRunningInGithubActions = true, + CancellationToken cancellationToken = default) + { + if (!Github.IsGithubActions && dryRunWhenNotRunningInGithubActions) + { + Logger.LogWarning("Not running in GitHub Actions, simulating publish of release {ReleaseId}.", releaseId); + + return null; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var update = new ReleaseUpdate + { + Draft = false, + }; + + return await GetOrCreateReleaseApi() + .EditRelease(ParseRepositoryId(), releaseId, update); + } + + /// + /// Returns the file names of all assets currently attached to a GitHub Release. + /// + /// The numeric identifier of the release. + /// + /// If true (default), the listing is simulated (logged but not executed) when not running in + /// GitHub Actions, and an empty list is returned. + /// + /// + /// A token that can be used to cancel the operation before it begins. + /// Note: cancellation is checked before the network call; the underlying HTTP request is not cancelled. + /// + /// + /// A read-only list of asset file names (the of each asset), + /// or an empty list when the operation was simulated or the release has no assets. + /// + /// Thrown if the GitHub repository ID cannot be parsed. + /// Thrown if is already cancelled. + [PublicAPI] + async Task> GetReleaseAssetNames( + long releaseId, + bool dryRunWhenNotRunningInGithubActions = true, + CancellationToken cancellationToken = default) + { + if (!Github.IsGithubActions && dryRunWhenNotRunningInGithubActions) + { + Logger.LogWarning("Not running in GitHub Actions, simulating asset listing for release {ReleaseId}.", + releaseId); + + return []; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var assets = await GetOrCreateReleaseApi() + .GetReleaseAssets(ParseRepositoryId(), releaseId); + + return assets + .Select(a => a.Name) + .ToList(); + } + + /// + /// Permanently deletes a GitHub Release. Use this for rollback when a release pipeline fails after + /// the release was created. + /// + /// The numeric identifier of the release to delete. + /// + /// If true (default), the deletion is simulated (logged but not executed) when not running in + /// GitHub Actions. + /// + /// + /// A token that can be used to cancel the operation before it begins. + /// Note: cancellation is checked before the network call; the underlying HTTP request is not cancelled. + /// + /// A that completes when the release has been deleted. + /// Thrown if the GitHub repository ID cannot be parsed. + /// Thrown if is already cancelled. + [PublicAPI] + async Task DeleteRelease( + long releaseId, + bool dryRunWhenNotRunningInGithubActions = true, + CancellationToken cancellationToken = default) + { + if (!Github.IsGithubActions && dryRunWhenNotRunningInGithubActions) + { + Logger.LogWarning("Not running in GitHub Actions, simulating deletion of release {ReleaseId}.", releaseId); + + return; + } + + cancellationToken.ThrowIfCancellationRequested(); + + await GetOrCreateReleaseApi() + .DeleteRelease(ParseRepositoryId(), releaseId); } /// @@ -112,6 +296,28 @@ async Task UploadArtifactToRelease( await UploadAssetToRelease(releaseTag, artifactPath, dryRunWhenNotRunningInGithubActions); } + /// + /// Uploads a build artifact to a specific GitHub Release. + /// + /// + /// The name of the artifact directory within the Atom artifacts directory. + /// + /// The GitHub Release to which the artifact should be uploaded. + /// + /// If true (default), the upload operation will be simulated when not running in GitHub Actions. + /// + /// A representing the asynchronous operation. + [PublicAPI] + async Task UploadArtifactToRelease( + string artifactName, + Release release, + bool dryRunWhenNotRunningInGithubActions = true) + { + var artifactPath = RootedFileSystem.AtomArtifactsDirectory / artifactName; + + await UploadAssetToRelease(release, artifactPath, dryRunWhenNotRunningInGithubActions); + } + /// /// Uploads a generic asset (file or directory) to a specified GitHub Release. /// @@ -152,11 +358,45 @@ async Task UploadAssetToRelease( return; } - var client = new GitHubClient(new("Invex.Atom"), new InMemoryCredentialStore(new(GithubToken))); + var api = GetOrCreateReleaseApi(); + var repositoryId = ParseRepositoryId(); + + var release = await api.FindReleaseByTag(repositoryId, releaseTag) ?? + throw new StepFailedException($"Could not find GitHub release with tag '{releaseTag}'."); + + await UploadAssetToReleaseCore(api, release, assetPath); + } + + /// + /// Uploads a generic asset to a specific GitHub Release. + /// + /// The GitHub Release to which the asset should be uploaded. + /// The file or directory to upload. + /// + /// If true (default), the upload operation will be simulated when not running in GitHub Actions. + /// + /// A representing the asynchronous operation. + [PublicAPI] + async Task UploadAssetToRelease( + Release release, + RootedPath assetPath, + bool dryRunWhenNotRunningInGithubActions = true) + { + if (!Github.IsGithubActions && dryRunWhenNotRunningInGithubActions) + { + Logger.LogWarning( + "Not running in GitHub Actions, simulating upload of artifact {AssetPath} to release {ReleaseTag}.", + assetPath, + release.TagName); + + return; + } - var releases = await client.Repository.Release.GetAll(long.Parse(Github.Variables.RepositoryId)); + await UploadAssetToReleaseCore(GetOrCreateReleaseApi(), release, assetPath); + } - var release = releases.FirstOrDefault(x => x.TagName == releaseTag); + private async Task UploadAssetToReleaseCore(IGithubReleaseApi api, Release release, RootedPath assetPath) + { if (assetPath.DirectoryExists) { @@ -192,6 +432,6 @@ async Task UploadAssetToRelease( await using var stream = assetFile.OpenRead(); var asset = new ReleaseAssetUpload(assetPath.FileName, "application/zip", stream, TimeSpan.FromMinutes(5)); - await client.Repository.Release.UploadAsset(release, asset); + await api.UploadAsset(release, asset); } } diff --git a/src/Invex.Atom.Module.GithubWorkflows/Helpers/OctokitGithubReleaseApi.cs b/src/Invex.Atom.Module.GithubWorkflows/Helpers/OctokitGithubReleaseApi.cs new file mode 100644 index 00000000..b2a3a64b --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Helpers/OctokitGithubReleaseApi.cs @@ -0,0 +1,43 @@ +namespace Invex.Atom.Module.GithubWorkflows.Helpers; + +/// +/// The default implementation that delegates all release operations to an +/// Octokit . +/// +/// The authenticated Octokit client used for all release API calls. +/// +/// To override the GitHub API behaviour in dependency injection (for example in tests), register a custom +/// before the host is built. When no registration is present, +/// creates a short-lived instance of this class automatically. +/// +[PublicAPI] +public sealed class OctokitGithubReleaseApi(GitHubClient client) : IGithubReleaseApi +{ + /// + public async Task CreateRelease(long repositoryId, NewRelease newRelease) => + await client.Repository.Release.Create(repositoryId, newRelease); + + /// + public async Task FindReleaseByTag(long repositoryId, string tagName) + { + var releases = await client.Repository.Release.GetAll(repositoryId); + + return releases.FirstOrDefault(r => string.Equals(r.TagName, tagName, StringComparison.Ordinal)); + } + + /// + public async Task EditRelease(long repositoryId, long releaseId, ReleaseUpdate update) => + await client.Repository.Release.Edit(repositoryId, releaseId, update); + + /// + public async Task> GetReleaseAssets(long repositoryId, long releaseId) => + await client.Repository.Release.GetAllAssets(repositoryId, releaseId); + + /// + public async Task DeleteRelease(long repositoryId, long releaseId) => + await client.Repository.Release.Delete(repositoryId, releaseId); + + /// + public async Task UploadAsset(Release release, ReleaseAssetUpload upload) => + await client.Repository.Release.UploadAsset(release, upload); +} diff --git a/src/Invex.Atom.Module.GithubWorkflows/Options/GithubConcurrencyOption.cs b/src/Invex.Atom.Module.GithubWorkflows/Options/GithubConcurrencyOption.cs new file mode 100644 index 00000000..8a7eb297 --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Options/GithubConcurrencyOption.cs @@ -0,0 +1,10 @@ +namespace Invex.Atom.Module.GithubWorkflows.Options; + +/// +/// Configures concurrency for a GitHub Actions workflow. +/// +/// The concurrency group key. +/// Whether to cancel an in-progress workflow in the same group. +[PublicAPI] +public sealed record GithubConcurrencyOption(TextExpression Group, TextExpression? CancelInProgress = null) + : IBuildOption; diff --git a/src/Invex.Atom.Module.GithubWorkflows/_usings.cs b/src/Invex.Atom.Module.GithubWorkflows/_usings.cs index 32a2f4da..88903525 100644 --- a/src/Invex.Atom.Module.GithubWorkflows/_usings.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/_usings.cs @@ -6,6 +6,7 @@ global using Invex.Atom.Build; global using Invex.Atom.Build.BuildOptions; global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.Exceptions; global using Invex.Atom.Build.Model; global using Invex.Atom.Build.Logging; global using Invex.Atom.Build.Params; diff --git a/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/ConcurrencyBuild.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/ConcurrencyBuild.cs new file mode 100644 index 00000000..441362bd --- /dev/null +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/ConcurrencyBuild.cs @@ -0,0 +1,21 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class ConcurrencyBuild : WorkflowBuildDefinition, IDevopsWorkflows, IConcurrencyTarget +{ + public override IReadOnlyList Workflows => + [ + new("concurrency-workflow") + { + Triggers = [WorkflowTriggers.PushToMain], + Targets = [new(nameof(IConcurrencyTarget.ConcurrencyTarget))], + Types = [WorkflowTypes.Devops.Pipeline], + Options = [BuildOptions.Devops.Concurrency.Sequential], + }, + ]; +} + +public interface IConcurrencyTarget +{ + Target ConcurrencyTarget => t => t; +} diff --git a/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/PullRequestAutoCancelBuild.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/PullRequestAutoCancelBuild.cs new file mode 100644 index 00000000..f9a51ac4 --- /dev/null +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/PullRequestAutoCancelBuild.cs @@ -0,0 +1,21 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class PullRequestAutoCancelBuild : WorkflowBuildDefinition, IDevopsWorkflows, IPullRequestTarget +{ + public override IReadOnlyList Workflows => + [ + new("pull-request-auto-cancel") + { + Triggers = [WorkflowTriggers.PullInto("main", "develop")], + Targets = [new(nameof(IPullRequestTarget.PullRequestTarget))], + Types = [WorkflowTypes.Devops.Pipeline], + Options = [BuildOptions.Devops.PullRequest.AutoCancel], + }, + ]; +} + +public interface IPullRequestTarget +{ + Target PullRequestTarget => t => t; +} diff --git a/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ConcurrencyBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ConcurrencyBuild_GeneratesWorkflow.verified.txt new file mode 100644 index 00000000..1b27bd70 --- /dev/null +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ConcurrencyBuild_GeneratesWorkflow.verified.txt @@ -0,0 +1,22 @@ +name: concurrency-workflow + +trigger: + branches: + include: [ main ] +lockBehavior: sequential + +jobs: + + - job: ConcurrencyTarget + displayName: ConcurrencyTarget + pool: + vmImage: ubuntu-latest + steps: + + - checkout: self + enabled: true + fetchDepth: 0 + + - script: dotnet run --project AtomTest/AtomTest.csproj ConcurrencyTarget --skip --headless + displayName: ConcurrencyTarget + name: ConcurrencyTarget diff --git a/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.PullRequestAutoCancelBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.PullRequestAutoCancelBuild_GeneratesWorkflow.verified.txt new file mode 100644 index 00000000..8d70d4cf --- /dev/null +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.PullRequestAutoCancelBuild_GeneratesWorkflow.verified.txt @@ -0,0 +1,23 @@ +name: pull-request-auto-cancel + +trigger: none +pr: + autoCancel: true + branches: + include: [ main, develop ] + +jobs: + + - job: PullRequestTarget + displayName: PullRequestTarget + pool: + vmImage: ubuntu-latest + steps: + + - checkout: self + enabled: true + fetchDepth: 0 + + - script: dotnet run --project AtomTest/AtomTest.csproj PullRequestTarget --skip --headless + displayName: PullRequestTarget + name: PullRequestTarget diff --git a/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.cs index 4a4d14af..45cbf642 100644 --- a/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.cs +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.cs @@ -53,6 +53,44 @@ public async Task SimpleBuild_GeneratesWorkflow() await TestContext.Out.WriteAsync(workflow); } + [Test] + public async Task ConcurrencyBuild_GeneratesWorkflow() + { + // Arrange + var fileSystem = FileSystemUtils.DefaultMockFileSystem; + + var build = CreateTestHost(fileSystem: fileSystem, + commandLineArgs: new(true, [new CommandArg(nameof(IGen.Gen))])); + + // Act + await build.RunAsync(); + + // Assert + var workflow = await fileSystem.File.ReadAllTextAsync($"{WorkflowDir}concurrency-workflow.yml"); + + await Verify(workflow); + await TestContext.Out.WriteAsync(workflow); + } + + [Test] + public async Task PullRequestAutoCancelBuild_GeneratesWorkflow() + { + // Arrange + var fileSystem = FileSystemUtils.DefaultMockFileSystem; + + var build = CreateTestHost(fileSystem: fileSystem, + commandLineArgs: new(true, [new CommandArg(nameof(IGen.Gen))])); + + // Act + await build.RunAsync(); + + // Assert + var workflow = await fileSystem.File.ReadAllTextAsync($"{WorkflowDir}pull-request-auto-cancel.yml"); + + await Verify(workflow); + await TestContext.Out.WriteAsync(workflow); + } + [Test] public async Task DependentBuild_GeneratesWorkflow() { diff --git a/tests/Invex.Atom.Module.Dotnet.Tests/Invex.Atom.Module.Dotnet.Tests.csproj b/tests/Invex.Atom.Module.Dotnet.Tests/Invex.Atom.Module.Dotnet.Tests.csproj new file mode 100644 index 00000000..6a3a4038 --- /dev/null +++ b/tests/Invex.Atom.Module.Dotnet.Tests/Invex.Atom.Module.Dotnet.Tests.csproj @@ -0,0 +1,56 @@ + + + + $(NoWarn);CS1591; + net10.0;net9.0;net8.0 + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/tests/Invex.Atom.Module.Dotnet.Tests/NugetHelperBuild.cs b/tests/Invex.Atom.Module.Dotnet.Tests/NugetHelperBuild.cs new file mode 100644 index 00000000..b291015a --- /dev/null +++ b/tests/Invex.Atom.Module.Dotnet.Tests/NugetHelperBuild.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Module.Dotnet.Tests; + +[BuildDefinition] +internal sealed partial class NugetHelperBuild : BuildDefinition, INugetHelper; diff --git a/tests/Invex.Atom.Module.Dotnet.Tests/PushPackageToNugetTests.cs b/tests/Invex.Atom.Module.Dotnet.Tests/PushPackageToNugetTests.cs new file mode 100644 index 00000000..5f5f426b --- /dev/null +++ b/tests/Invex.Atom.Module.Dotnet.Tests/PushPackageToNugetTests.cs @@ -0,0 +1,148 @@ +namespace Invex.Atom.Module.Dotnet.Tests; + +[TestFixture] +internal sealed class PushPackageToNugetTests +{ + // A package path constructed from the default mock file system used by CreateTestHost. + private static readonly RootedPath TestPackagePath = new(FileSystemUtils.DefaultMockFileSystem, + Environment.OSVersion.Platform is PlatformID.Win32NT + ? @"C:\Atom\artifacts\MyPackage\MyPackage.1.0.0.nupkg" + : "/Atom/artifacts/MyPackage/MyPackage.1.0.0.nupkg"); + + private const string Feed = "https://api.nuget.org/v3/index.json"; + private const string ApiKey = "secret-api-key-abc123"; + + private static IProcessRunner FakeRunnerReturning(int exitCode, string stdout = "", string stderr = "") + { + var fake = A.Fake(); + + A + .CallTo(() => fake.RunAsync(A._, A._)) + .Returns(Task.FromResult(new ProcessRunResult(new("dotnet", "test"), exitCode, stdout, stderr))); + + return fake; + } + + private static INugetHelper CreateSut(IProcessRunner processRunner, CommandLineArgs? commandLineArgs = null) + { + commandLineArgs ??= new(true, [new ParamArg("nuget-dry-run", nameof(INugetHelper.NugetDryRun), "false")]); + + var host = CreateTestHost(commandLineArgs: commandLineArgs, + configure: builder => builder.Services.AddSingleton(processRunner)); + + return (INugetHelper)host.Services.GetRequiredService(); + } + + [Test] + public async Task PushPackageToNuget_WhenProcessExitsZero_DoesNotThrow() + { + // Arrange + var sut = CreateSut(FakeRunnerReturning(0)); + + // Act / Assert + await Should.NotThrowAsync(() => sut.PushPackageToNuget(TestPackagePath, Feed, ApiKey)); + } + + [Test] + public async Task PushPackageToNuget_WhenProcessExitsZero_InvokesRunAsyncOnce() + { + // Arrange + var fakeRunner = FakeRunnerReturning(0); + var sut = CreateSut(fakeRunner); + + // Act + await sut.PushPackageToNuget(TestPackagePath, Feed, ApiKey); + + // Assert + A + .CallTo(() => fakeRunner.RunAsync(A._, A._)) + .MustHaveHappenedOnceExactly(); + } + + [Test] + public async Task PushPackageToNuget_WhenDryRunEnabled_DoesNotInvokeRunAsync() + { + // Arrange + var fakeRunner = FakeRunnerReturning(0); + + var dryRunArgs = new CommandLineArgs(true, + [new ParamArg("nuget-dry-run", nameof(INugetHelper.NugetDryRun), "true")]); + + var sut = CreateSut(fakeRunner, dryRunArgs); + + // Act + await sut.PushPackageToNuget(TestPackagePath, Feed, ApiKey); + + // Assert + A + .CallTo(() => fakeRunner.RunAsync(A._, A._)) + .MustNotHaveHappened(); + } + + [Test] + public async Task PushPackageToNuget_WhenCancelled_PropagatesOperationCanceledException() + { + // Arrange + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var fakeRunner = A.Fake(); + + A + .CallTo(() => fakeRunner.RunAsync(A._, A._)) + .Returns(Task.FromCanceled(cts.Token)); + + var sut = CreateSut(fakeRunner); + + // Act / Assert + await Should.ThrowAsync(() => + sut.PushPackageToNuget(TestPackagePath, Feed, ApiKey, cancellationToken: cts.Token)); + } + + [Test] + public async Task PushPackageToNuget_WhenProcessExitsNonZero_ThrowsStepFailedException() + { + // Arrange + var sut = CreateSut(FakeRunnerReturning(1, stderr: "Push rejected by server.")); + + // Act / Assert + var ex = await Should.ThrowAsync(() => + sut.PushPackageToNuget(TestPackagePath, Feed, ApiKey)); + + ex.Message.ShouldContain("exit code 1"); + } + + [Test] + public async Task PushPackageToNuget_WhenProcessExitsNonZero_ExceptionMessageContainsSanitizedStderr() + { + // Arrange + const string stderrWithKey = $"Authentication failed: key={ApiKey} is invalid."; + + var sut = CreateSut(FakeRunnerReturning(401, stderr: stderrWithKey)); + + // Act + var ex = await Should.ThrowAsync(() => + sut.PushPackageToNuget(TestPackagePath, Feed, ApiKey)); + + // Assert + ex.Message.ShouldContain("exit code 401"); + ex.Message.ShouldNotContain(ApiKey); + ex.Message.ShouldContain("[REDACTED]"); + } + + [Test] + public async Task PushPackageToNuget_WhenProcessExitsNonZero_StderrWithoutKeyIsPreserved() + { + // Arrange + const string stderrClean = "Package version already exists on the feed."; + + var sut = CreateSut(FakeRunnerReturning(1, stderr: stderrClean)); + + // Act + var ex = await Should.ThrowAsync(() => + sut.PushPackageToNuget(TestPackagePath, Feed, ApiKey)); + + // Assert + ex.Message.ShouldContain(stderrClean); + } +} diff --git a/tests/Invex.Atom.Module.Dotnet.Tests/_usings.cs b/tests/Invex.Atom.Module.Dotnet.Tests/_usings.cs new file mode 100644 index 00000000..d9b30348 --- /dev/null +++ b/tests/Invex.Atom.Module.Dotnet.Tests/_usings.cs @@ -0,0 +1,14 @@ +global using System.IO.Abstractions.TestingHelpers; +global using Invex.Atom.Build; +global using Invex.Atom.Build.Args; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.Exceptions; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Module.Dotnet.Helpers; +global using Invex.Atom.TestUtils; +global using FakeItEasy; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using NUnit.Framework; +global using Shouldly; +global using static Invex.Atom.TestUtils.TestUtils; diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Helpers/GithubReleaseHelperTests.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Helpers/GithubReleaseHelperTests.cs new file mode 100644 index 00000000..b6275599 --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Helpers/GithubReleaseHelperTests.cs @@ -0,0 +1,413 @@ +using System.IO.Abstractions.TestingHelpers; +using System.Reflection; +using System.Runtime.CompilerServices; +using FakeItEasy; +using Invex.Atom.Build.Exceptions; +using Octokit; +using Environment = System.Environment; + +namespace Invex.Atom.Module.GithubWorkflows.Tests.Helpers; + +/// +/// Minimal implementation that wires the interface to a custom +/// so tests can inject a mock . +/// +internal sealed class ReleaseHelperSubject(IServiceProvider services) : IGithubReleaseHelper +{ + public IServiceProvider Services => services; +} + +[TestFixture] +[NonParallelizable] +internal sealed class GithubReleaseHelperTests +{ + private const long RepositoryId = 99999L; + private string? _originalGithubActions; + private string? _originalRepositoryId; + + // Returns an IGithubReleaseHelper so default interface methods are callable without an extra cast. + private static IGithubReleaseHelper BuildHelper(IGithubReleaseApi? api = null) + { + var sc = new ServiceCollection().AddLogging(); + + if (api is not null) + sc.AddSingleton(api); + + return new ReleaseHelperSubject(sc.BuildServiceProvider()); + } + + [SetUp] + public void SetUp() + { + _originalGithubActions = Environment.GetEnvironmentVariable("GITHUB_ACTIONS"); + _originalRepositoryId = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY_ID"); + Environment.SetEnvironmentVariable("GITHUB_ACTIONS", "false"); + Environment.SetEnvironmentVariable("GITHUB_REPOSITORY_ID", RepositoryId.ToString()); + } + + [TearDown] + public void TearDown() + { + Environment.SetEnvironmentVariable("GITHUB_ACTIONS", _originalGithubActions); + Environment.SetEnvironmentVariable("GITHUB_REPOSITORY_ID", _originalRepositoryId); + } + + // CreateRelease payload and exact target + + [Test] + public async Task CreateRelease_DraftTrue_PassesDraftPayloadToApi() + { + // Arrange + var expectedRelease = CreateFakeRelease(1, "v1.0.0"); + NewRelease? captured = null; + var api = A.Fake(); + + A + .CallTo(() => api.CreateRelease(RepositoryId, A._)) + .Invokes((long _, NewRelease nr) => captured = nr) + .Returns(expectedRelease); + + var helper = BuildHelper(api); + + // Act + var result = await helper.CreateRelease("v1.0.0", draft: true, dryRunWhenNotRunningInGithubActions: false); + + // Assert + result.ShouldBe(expectedRelease); + captured.ShouldNotBeNull(); + captured.Draft.ShouldBeTrue(); + captured.TagName.ShouldBe("v1.0.0"); + } + + [Test] + public async Task CreateRelease_WithTargetCommitish_SetsExactTargetOnPayload() + { + // Arrange + NewRelease? captured = null; + var api = A.Fake(); + + A + .CallTo(() => api.CreateRelease(RepositoryId, A._)) + .Invokes((long _, NewRelease nr) => captured = nr) + .Returns(CreateFakeRelease(2, "v2.0.0")); + + var helper = BuildHelper(api); + + // Act + await helper.CreateRelease("v2.0.0", "refs/heads/release/2.0", dryRunWhenNotRunningInGithubActions: false); + + // Assert + captured.ShouldNotBeNull(); + captured.TargetCommitish.ShouldBe("refs/heads/release/2.0"); + } + + [Test] + public async Task CreateRelease_WhenNotInGithubActions_ReturnsDryRunNull() + { + // Arrange + var helper = BuildHelper(); + + // Act + var result = await helper.CreateRelease("v1.0.0"); // dryRun = true (default) + + // Assert + result.ShouldBeNull(); + } + + // FindRelease lookup and not-found behavior + + [Test] + public async Task FindRelease_WhenReleaseExists_ReturnsMatchedRelease() + { + // Arrange + var expected = CreateFakeRelease(10, "v1.0.0"); + var api = A.Fake(); + + A + .CallTo(() => api.FindReleaseByTag(RepositoryId, "v1.0.0")) + .Returns(expected); + + var helper = BuildHelper(api); + + // Act + var result = await helper.FindRelease("v1.0.0", false); + + // Assert + result.ShouldBe(expected); + } + + [Test] + public async Task FindRelease_WhenReleaseDoesNotExist_ReturnsNull() + { + // Arrange + var api = A.Fake(); + + A + .CallTo(() => api.FindReleaseByTag(RepositoryId, "v99.0.0")) + .Returns(Task.FromResult(null)); + + var helper = BuildHelper(api); + + // Act + var result = await helper.FindRelease("v99.0.0", false); + + // Assert + result.ShouldBeNull(); + } + + [Test] + public async Task FindRelease_WhenNotInGithubActions_ReturnsDryRunNull() + { + // Arrange + var api = A.Fake(); + var helper = BuildHelper(api); + + // Act + var result = await helper.FindRelease("v1.0.0"); // dryRun = true (default) + + // Assert + result.ShouldBeNull(); + + A + .CallTo(() => api.FindReleaseByTag(A._, A._)) + .MustNotHaveHappened(); + } + + [Test] + public void FindRelease_WithCancelledToken_ThrowsOperationCancelled() + { + // Arrange + var helper = BuildHelper(A.Fake()); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act / Assert + Should.ThrowAsync(() => helper.FindRelease("v1.0.0", false, cts.Token)); + } + + [Test] + public async Task UploadAssetToRelease_WhenReleaseDoesNotExist_ThrowsActionableFailure() + { + var api = A.Fake(); + + A + .CallTo(() => api.FindReleaseByTag(RepositoryId, "v1.0.0")) + .Returns(Task.FromResult(null)); + + var helper = BuildHelper(api); + var fileSystem = new MockFileSystem(); + + var assetPath = new RootedPath(fileSystem, + Environment.OSVersion.Platform is PlatformID.Win32NT + ? @"C:\artifact.zip" + : "/artifact.zip"); + + var exception = await Should.ThrowAsync(() => + helper.UploadAssetToRelease("v1.0.0", assetPath, false)); + + exception.Message.ShouldContain("v1.0.0"); + } + + // PublishRelease draft-to-published transition + + [Test] + public async Task PublishRelease_SetsReleaseUpdateDraftToFalse() + { + // Arrange + ReleaseUpdate? captured = null; + var published = CreateFakeRelease(5, "v3.0.0"); + var api = A.Fake(); + + A + .CallTo(() => api.EditRelease(RepositoryId, 5, A._)) + .Invokes((long _, long _, ReleaseUpdate u) => captured = u) + .Returns(published); + + var helper = BuildHelper(api); + + // Act + var result = await helper.PublishRelease(5, false); + + // Assert + result.ShouldBe(published); + captured.ShouldNotBeNull(); + captured.Draft.ShouldBe(false); + } + + [Test] + public async Task PublishRelease_WhenNotInGithubActions_ReturnsDryRunNull() + { + // Arrange + var api = A.Fake(); + var helper = BuildHelper(api); + + // Act + var result = await helper.PublishRelease(42); // dryRun = true (default) + + // Assert + result.ShouldBeNull(); + + A + .CallTo(() => api.EditRelease(A._, A._, A._)) + .MustNotHaveHappened(); + } + + [Test] + public void PublishRelease_WithCancelledToken_ThrowsOperationCancelled() + { + // Arrange + var helper = BuildHelper(A.Fake()); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act / Assert + Should.ThrowAsync(() => helper.PublishRelease(1, false, cts.Token)); + } + + // GetReleaseAssetNames + + [Test] + public async Task GetReleaseAssetNames_ReturnsNamesOfAllAttachedAssets() + { + // Arrange + var assets = new List + { + CreateFakeAsset("package.zip"), + CreateFakeAsset("checksums.txt"), + }; + + var api = A.Fake(); + + A + .CallTo(() => api.GetReleaseAssets(RepositoryId, 7)) + .Returns(assets); + + var helper = BuildHelper(api); + + // Act + var names = await helper.GetReleaseAssetNames(7, false); + + // Assert + names.ShouldBe(["package.zip", "checksums.txt"], false); + } + + [Test] + public async Task GetReleaseAssetNames_WhenReleaseHasNoAssets_ReturnsEmptyList() + { + // Arrange + var api = A.Fake(); + + A + .CallTo(() => api.GetReleaseAssets(RepositoryId, 8)) + .Returns(new List()); + + var helper = BuildHelper(api); + + // Act + var names = await helper.GetReleaseAssetNames(8, false); + + // Assert + names.ShouldBeEmpty(); + } + + [Test] + public async Task GetReleaseAssetNames_WhenNotInGithubActions_ReturnsDryRunEmptyList() + { + // Arrange + var api = A.Fake(); + var helper = BuildHelper(api); + + // Act + var names = await helper.GetReleaseAssetNames(99); // dryRun = true (default) + + // Assert + names.ShouldBeEmpty(); + + A + .CallTo(() => api.GetReleaseAssets(A._, A._)) + .MustNotHaveHappened(); + } + + // DeleteRelease cleanup and rollback + + [Test] + public async Task DeleteRelease_CallsApiDeleteWithCorrectReleaseId() + { + // Arrange + var api = A.Fake(); + + A + .CallTo(() => api.DeleteRelease(RepositoryId, 20)) + .Returns(Task.CompletedTask); + + var helper = BuildHelper(api); + + // Act + await helper.DeleteRelease(20, false); + + // Assert + A + .CallTo(() => api.DeleteRelease(RepositoryId, 20)) + .MustHaveHappenedOnceExactly(); + } + + [Test] + public async Task DeleteRelease_WhenNotInGithubActions_SkipsApiCall() + { + // Arrange + var api = A.Fake(); + var helper = BuildHelper(api); + + // Act + await helper.DeleteRelease(99); // dryRun = true (default) + + // Assert + A + .CallTo(() => api.DeleteRelease(A._, A._)) + .MustNotHaveHappened(); + } + + [Test] + public void DeleteRelease_WithCancelledToken_ThrowsOperationCancelled() + { + // Arrange + var helper = BuildHelper(A.Fake()); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act / Assert + Should.ThrowAsync(() => helper.DeleteRelease(1, false, cts.Token)); + } + + // Test helpers + + private static Release CreateFakeRelease(int id, string tagName) + { + // Octokit Release has no public parameterless constructor. + var release = (Release)RuntimeHelpers.GetUninitializedObject(typeof(Release)); + SetProperty(release, "Id", id); + SetProperty(release, "TagName", tagName); + SetProperty(release, "Draft", false); + + return release; + } + + private static ReleaseAsset CreateFakeAsset(string name) + { + var asset = (ReleaseAsset)RuntimeHelpers.GetUninitializedObject(typeof(ReleaseAsset)); + SetProperty(asset, "Name", name); + + return asset; + } + + private static void SetProperty(object obj, string propertyName, object? value) + { + var prop = obj + .GetType() + .GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); + + prop + ?.GetSetMethod(true) + ?.Invoke(obj, [value]); + } +} diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ConcurrencyBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ConcurrencyBuild.cs new file mode 100644 index 00000000..873d4c2d --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ConcurrencyBuild.cs @@ -0,0 +1,29 @@ +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class ConcurrencyBuild : WorkflowBuildDefinition, IGithubWorkflows, IConcurrencyTarget +{ + public override IReadOnlyList Workflows => + [ + new("concurrency-workflow") + { + Triggers = [WorkflowTriggers.PushToMain], + Targets = [new(nameof(IConcurrencyTarget.ConcurrencyTarget))], + Types = [WorkflowTypes.Github.Action], + Options = + [ + BuildOptions.Github.Concurrency.Set(TextExpressions.Concat([ + TextExpressions.Github.GithubWorkflow.Evaluate(), + "-", + TextExpressions.Github.GithubRef.Evaluate(), + ]), + true), + ], + }, + ]; +} + +public interface IConcurrencyTarget +{ + Target ConcurrencyTarget => t => t; +} diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.ConcurrencyBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.ConcurrencyBuild_GeneratesWorkflow.verified.txt new file mode 100644 index 00000000..d629e8fd --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.ConcurrencyBuild_GeneratesWorkflow.verified.txt @@ -0,0 +1,27 @@ +name: concurrency-workflow + +on: + push: + branches: [ main ] + +permissions: { } + +concurrency: + group: '${{ github.workflow }}-${{ github.ref }}' + cancel-in-progress: true + +jobs: + + ConcurrencyTarget: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: ConcurrencyTarget + id: ConcurrencyTarget + run: dotnet run --project AtomTest/AtomTest.csproj -- ConcurrencyTarget --skip --headless + diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.cs index 508dc8d7..d95b3473 100644 --- a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.cs +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.cs @@ -60,6 +60,25 @@ public async Task SimpleBuild_GeneratesWorkflow() await TestContext.Out.WriteAsync(workflow); } + [Test] + public async Task ConcurrencyBuild_GeneratesWorkflow() + { + // Arrange + var fileSystem = FileSystemUtils.DefaultMockFileSystem; + + var build = CreateTestHost(fileSystem: fileSystem, + commandLineArgs: new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))])); + + // Act + await build.RunAsync(); + + // Assert + var workflow = await fileSystem.File.ReadAllTextAsync($"{WorkflowDir}concurrency-workflow.yml"); + + await Verify(workflow); + await TestContext.Out.WriteAsync(workflow); + } + [Test] public async Task DependentBuild_GeneratesWorkflow() {