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()
{