diff --git a/.devops/workflows/Test_Devops_Build.yml b/.devops/workflows/Test_Devops_Build.yml index ca734386..343b5e7d 100644 --- a/.devops/workflows/Test_Devops_Build.yml +++ b/.devops/workflows/Test_Devops_Build.yml @@ -1,92 +1,108 @@ name: Test_Devops_Build -variables: - - group: Atom trigger: branches: - include: - - 'main' + include: [ main ] +pr: + branches: + include: [ main ] +variables: + - group: Atom jobs: - + - job: SetupBuildInfo + displayName: SetupBuildInfo pool: vmImage: ubuntu-latest steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - task: UseDotNet@2 + displayName: Setup .NET 10.0.x inputs: - version: '10.0.x' + version: 10.0.x - script: dotnet run --project _atom/_atom.csproj SetupBuildInfo --skip --headless + displayName: SetupBuildInfo name: SetupBuildInfo env: nuget-dry-run: true - + - job: PackProjects + displayName: PackProjects pool: vmImage: ubuntu-latest steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - task: UseDotNet@2 + displayName: Setup .NET 10.0.x inputs: - version: '10.0.x' + version: 10.0.x - script: dotnet run --project _atom/_atom.csproj PackProjects --skip --headless + displayName: PackProjects name: PackProjects env: nuget-dry-run: true - + + - task: PublishPipelineArtifact@1 + displayName: Invex.Atom.Build + inputs: + artifactName: Invex.Atom.Build + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Build + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom + displayName: Invex.Atom.Workflows inputs: - artifactName: DecSm.Atom - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom" - + artifactName: Invex.Atom.Workflows + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Workflows + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom.Module.AzureKeyVault + displayName: Invex.Atom.Module.AzureKeyVault inputs: - artifactName: DecSm.Atom.Module.AzureKeyVault - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom.Module.AzureKeyVault" - + artifactName: Invex.Atom.Module.AzureKeyVault + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Module.AzureKeyVault + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom.Module.AzureStorage + displayName: Invex.Atom.Module.AzureStorage inputs: - artifactName: DecSm.Atom.Module.AzureStorage - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom.Module.AzureStorage" - + artifactName: Invex.Atom.Module.AzureStorage + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Module.AzureStorage + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom.Module.DevopsWorkflows + displayName: Invex.Atom.Module.DevopsWorkflows inputs: - artifactName: DecSm.Atom.Module.DevopsWorkflows - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom.Module.DevopsWorkflows" - + artifactName: Invex.Atom.Module.DevopsWorkflows + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Module.DevopsWorkflows + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom.Module.Dotnet + displayName: Invex.Atom.Module.Dotnet inputs: - artifactName: DecSm.Atom.Module.Dotnet - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom.Module.Dotnet" - + artifactName: Invex.Atom.Module.Dotnet + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Module.Dotnet + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom.Module.GitVersion + displayName: Invex.Atom.Module.GitVersion inputs: - artifactName: DecSm.Atom.Module.GitVersion - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom.Module.GitVersion" - + artifactName: Invex.Atom.Module.GitVersion + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Module.GitVersion + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom.Module.GithubWorkflows + displayName: Invex.Atom.Module.GithubWorkflows inputs: - artifactName: DecSm.Atom.Module.GithubWorkflows - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom.Module.GithubWorkflows" - - + artifactName: Invex.Atom.Module.GithubWorkflows + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Module.GithubWorkflows + - job: PackTool + displayName: PackTool strategy: matrix: 001_windows-latest: @@ -98,29 +114,32 @@ jobs: pool: vmImage: $(job-runs-on) steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - task: UseDotNet@2 + displayName: Setup .NET 10.0.x inputs: - version: '10.0.x' + version: 10.0.x - script: dotnet run --project _atom/_atom.csproj PackTool --skip --headless + displayName: PackTool name: PackTool env: nuget-dry-run: true job-runs-on: $(job-runs-on) build-slice: $(job-runs-on) - + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom.Tool + displayName: Invex.Atom.Tool inputs: - artifactName: DecSm.Atom.Tool-$(job-runs-on) - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom.Tool" - - + artifactName: Invex.Atom.Tool-$(job-runs-on) + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Tool + - job: TestProjects + displayName: TestProjects strategy: matrix: 001_windows-latest_net8-0: @@ -153,148 +172,166 @@ jobs: pool: vmImage: $(job-runs-on) steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - task: UseDotNet@2 + displayName: Setup .NET 10.0.x inputs: - version: '10.0.x' + version: 10.0.x - task: UseDotNet@2 + displayName: Setup .NET 8.0.x inputs: - version: '8.0.x' + version: 8.0.x - task: UseDotNet@2 + displayName: Setup .NET 9.0.x inputs: - version: '9.0.x' + version: 9.0.x - script: dotnet run --project _atom/_atom.csproj TestProjects --skip --headless + displayName: TestProjects name: TestProjects env: nuget-dry-run: true job-runs-on: $(job-runs-on) test-framework: $(test-framework) build-slice: $(job-runs-on)-$(test-framework) - + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom.Tests + displayName: Invex.Atom.Build.Tests inputs: - artifactName: DecSm.Atom.Tests-$(job-runs-on)-$(test-framework) - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom.Tests" - + artifactName: Invex.Atom.Build.Tests-$(job-runs-on)-$(test-framework) + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Build.Tests + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom.Analyzers.Tests + displayName: Invex.Atom.Build.Analyzers.Tests inputs: - artifactName: DecSm.Atom.Analyzers.Tests-$(job-runs-on)-$(test-framework) - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom.Analyzers.Tests" - + artifactName: Invex.Atom.Build.Analyzers.Tests-$(job-runs-on)-$(test-framework) + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Build.Analyzers.Tests + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom.SourceGenerators.Tests + displayName: Invex.Atom.Build.SourceGenerators.Tests inputs: - artifactName: DecSm.Atom.SourceGenerators.Tests-$(job-runs-on)-$(test-framework) - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom.SourceGenerators.Tests" - + artifactName: Invex.Atom.Build.SourceGenerators.Tests-$(job-runs-on)-$(test-framework) + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Build.SourceGenerators.Tests + + - task: PublishPipelineArtifact@1 + displayName: Invex.Atom.Module.DevopsWorkflows.Tests + inputs: + artifactName: Invex.Atom.Module.DevopsWorkflows.Tests-$(job-runs-on)-$(test-framework) + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Module.DevopsWorkflows.Tests + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom.Module.DevopsWorkflows.Tests + displayName: Invex.Atom.Module.GithubWorkflows.Tests inputs: - artifactName: DecSm.Atom.Module.DevopsWorkflows.Tests-$(job-runs-on)-$(test-framework) - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom.Module.DevopsWorkflows.Tests" - + artifactName: Invex.Atom.Module.GithubWorkflows.Tests-$(job-runs-on)-$(test-framework) + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Module.GithubWorkflows.Tests + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom.Module.GithubWorkflows.Tests + displayName: Invex.Atom.Workflows.Tests inputs: - artifactName: DecSm.Atom.Module.GithubWorkflows.Tests-$(job-runs-on)-$(test-framework) - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom.Module.GithubWorkflows.Tests" - + artifactName: Invex.Atom.Workflows.Tests-$(job-runs-on)-$(test-framework) + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Workflows.Tests + - task: PublishPipelineArtifact@1 - displayName: DecSm.Atom.Tool.Tests + displayName: Invex.Atom.Tool.Tests inputs: - artifactName: DecSm.Atom.Tool.Tests-$(job-runs-on)-$(test-framework) - targetPath: "$(Build.BinariesDirectory)/DecSm.Atom.Tool.Tests" - - + artifactName: Invex.Atom.Tool.Tests-$(job-runs-on)-$(test-framework) + targetPath: $(Build.BinariesDirectory)/Invex.Atom.Tool.Tests + - job: PushToNugetDevops + displayName: PushToNugetDevops dependsOn: [ TestProjects, PackProjects, PackTool, SetupBuildInfo ] + variables: + - name: build-id + value: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-id'] ] pool: vmImage: ubuntu-latest - variables: - build-id: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-id'] ] steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - task: UseDotNet@2 + displayName: Setup .NET 10.0.x inputs: - version: '10.0.x' + version: 10.0.x - task: DownloadPipelineArtifact@2 - displayName: DecSm.Atom + displayName: Invex.Atom.Build inputs: - artifact: DecSm.Atom - path: "$(Build.ArtifactStagingDirectory)/DecSm.Atom" - + artifact: Invex.Atom.Build + path: $(Build.ArtifactStagingDirectory)/Invex.Atom.Build + - task: DownloadPipelineArtifact@2 - displayName: DecSm.Atom.Module.AzureKeyVault + displayName: Invex.Atom.Workflows inputs: - artifact: DecSm.Atom.Module.AzureKeyVault - path: "$(Build.ArtifactStagingDirectory)/DecSm.Atom.Module.AzureKeyVault" - + artifact: Invex.Atom.Workflows + path: $(Build.ArtifactStagingDirectory)/Invex.Atom.Workflows + - task: DownloadPipelineArtifact@2 - displayName: DecSm.Atom.Module.AzureStorage + displayName: Invex.Atom.Module.AzureKeyVault inputs: - artifact: DecSm.Atom.Module.AzureStorage - path: "$(Build.ArtifactStagingDirectory)/DecSm.Atom.Module.AzureStorage" - + artifact: Invex.Atom.Module.AzureKeyVault + path: $(Build.ArtifactStagingDirectory)/Invex.Atom.Module.AzureKeyVault + - task: DownloadPipelineArtifact@2 - displayName: DecSm.Atom.Module.DevopsWorkflows + displayName: Invex.Atom.Module.AzureStorage inputs: - artifact: DecSm.Atom.Module.DevopsWorkflows - path: "$(Build.ArtifactStagingDirectory)/DecSm.Atom.Module.DevopsWorkflows" - + artifact: Invex.Atom.Module.AzureStorage + path: $(Build.ArtifactStagingDirectory)/Invex.Atom.Module.AzureStorage + - task: DownloadPipelineArtifact@2 - displayName: DecSm.Atom.Module.Dotnet + displayName: Invex.Atom.Module.DevopsWorkflows inputs: - artifact: DecSm.Atom.Module.Dotnet - path: "$(Build.ArtifactStagingDirectory)/DecSm.Atom.Module.Dotnet" - + artifact: Invex.Atom.Module.DevopsWorkflows + path: $(Build.ArtifactStagingDirectory)/Invex.Atom.Module.DevopsWorkflows + - task: DownloadPipelineArtifact@2 - displayName: DecSm.Atom.Module.GitVersion + displayName: Invex.Atom.Module.Dotnet inputs: - artifact: DecSm.Atom.Module.GitVersion - path: "$(Build.ArtifactStagingDirectory)/DecSm.Atom.Module.GitVersion" - + artifact: Invex.Atom.Module.Dotnet + path: $(Build.ArtifactStagingDirectory)/Invex.Atom.Module.Dotnet + - task: DownloadPipelineArtifact@2 - displayName: DecSm.Atom.Module.GithubWorkflows + displayName: Invex.Atom.Module.GitVersion inputs: - artifact: DecSm.Atom.Module.GithubWorkflows - path: "$(Build.ArtifactStagingDirectory)/DecSm.Atom.Module.GithubWorkflows" - + artifact: Invex.Atom.Module.GitVersion + path: $(Build.ArtifactStagingDirectory)/Invex.Atom.Module.GitVersion + - task: DownloadPipelineArtifact@2 - displayName: DecSm.Atom.Tool + displayName: Invex.Atom.Module.GithubWorkflows inputs: - artifact: DecSm.Atom.Tool-windows-latest - path: "$(Build.ArtifactStagingDirectory)/DecSm.Atom.Tool" - + artifact: Invex.Atom.Module.GithubWorkflows + path: $(Build.ArtifactStagingDirectory)/Invex.Atom.Module.GithubWorkflows + + - task: DownloadPipelineArtifact@2 + displayName: Invex.Atom.Tool + inputs: + artifact: Invex.Atom.Tool-windows-latest + path: $(Build.ArtifactStagingDirectory)/Invex.Atom.Tool + - task: DownloadPipelineArtifact@2 - displayName: DecSm.Atom.Tool + displayName: Invex.Atom.Tool inputs: - artifact: DecSm.Atom.Tool-ubuntu-latest - path: "$(Build.ArtifactStagingDirectory)/DecSm.Atom.Tool" - + artifact: Invex.Atom.Tool-ubuntu-latest + path: $(Build.ArtifactStagingDirectory)/Invex.Atom.Tool + - task: DownloadPipelineArtifact@2 - displayName: DecSm.Atom.Tool + displayName: Invex.Atom.Tool inputs: - artifact: DecSm.Atom.Tool-macos-latest - path: "$(Build.ArtifactStagingDirectory)/DecSm.Atom.Tool" - + artifact: Invex.Atom.Tool-macos-latest + path: $(Build.ArtifactStagingDirectory)/Invex.Atom.Tool + - script: dotnet run --project _atom/_atom.csproj PushToNugetDevops --skip --headless + displayName: PushToNugetDevops name: PushToNugetDevops env: build-id: $(build-id) - azure-vault-app-secret: $(AZURE_VAULT_APP_SECRET) - azure-vault-address: $(AZURE_VAULT_ADDRESS) - azure-vault-tenant-id: $(AZURE_VAULT_TENANT_ID) - azure-vault-app-id: $(AZURE_VAULT_APP_ID) + nuget-push-api-key: $(NUGET_PUSH_API_KEY) nuget-dry-run: true diff --git a/.editorconfig b/.editorconfig index c77b16ab..e938030e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -62,7 +62,7 @@ csharp_blank_lines_before_multiline_statements = 1 csharp_remove_blank_lines_near_braces_in_code = true csharp_remove_blank_lines_near_braces_in_declarations = true csharp_braces_for_ifelse = not_required_for_both # actually behaves as `required for either` -csharp_braces_for_using = required_for_multiline +csharp_braces_for_using = not_required csharp_braces_for_while = required_for_multiline csharp_empty_block_style = together_same_line csharp_indent_preprocessor_if = usual_indent @@ -123,6 +123,9 @@ dotnet_style_prefer_collection_expression = true dotnet_separate_import_directive_groups = false dotnet_sort_system_directives_first = true +# .NET code quality rules +dotnet_code_quality.DecSm_Analyzers_ValidPublicApiAttributes = UnstableAPI + [*.{received,verified}.{txt,xml,json}] # Verify settings end_of_line = lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c91404c1..9ed67f0b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,16 +6,14 @@ registries: url: https://api.nuget.org/v3/index.json updates: - - package-ecosystem: "nuget" - target-branch: 'main' - directory: '/' - registries: - - nuget + - package-ecosystem: nuget + directory: "/" + schedule: + interval: daily groups: nuget-deps: - patterns: - - '*' - schedule: - interval: - daily + patterns: [ "*" ] open-pull-requests-limit: 10 + registries: [ nuget ] + target-branch: main + diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml index b0aa0340..d6baf309 100644 --- a/.github/workflows/Build.yml +++ b/.github/workflows/Build.yml @@ -1,19 +1,16 @@ name: Build -permissions: { } on: - workflow_dispatch: + push: + branches: [ main, feature/**, patch/** ] release: types: [ released ] - - push: - branches: - - 'main' - - 'feature/**' - - 'patch/**' + workflow_dispatch: + +permissions: { } jobs: - + SetupBuildInfo: runs-on: ubuntu-latest outputs: @@ -22,1021 +19,1229 @@ jobs: build-version: ${{ steps.SetupBuildInfo.outputs.build-version }} build-timestamp: ${{ steps.SetupBuildInfo.outputs.build-timestamp }} steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '10.0.x' - + dotnet-version: 10.0.x + - name: SetupBuildInfo id: SetupBuildInfo - run: dotnet run --project _atom/_atom.csproj SetupBuildInfo --skip --headless - + run: dotnet run --project _atom/_atom.csproj -- SetupBuildInfo --skip --headless + PackProjects: runs-on: ubuntu-latest steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '10.0.x' - + dotnet-version: 10.0.x + - name: PackProjects id: PackProjects - run: dotnet run --project _atom/_atom.csproj PackProjects --skip --headless - - - name: Upload DecSm.Atom - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom - path: "${{ github.workspace }}/.github/publish/DecSm.Atom" - - - name: Upload DecSm.Atom.Module.AzureKeyVault - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.Module.AzureKeyVault - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Module.AzureKeyVault" - - - name: Upload DecSm.Atom.Module.AzureStorage - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.Module.AzureStorage - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Module.AzureStorage" - - - name: Upload DecSm.Atom.Module.DevopsWorkflows - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Module.DevopsWorkflows" - - - name: Upload DecSm.Atom.Module.Dotnet - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.Module.Dotnet - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Module.Dotnet" - - - name: Upload DecSm.Atom.Module.GitVersion - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.Module.GitVersion - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Module.GitVersion" - - - name: Upload DecSm.Atom.Module.GithubWorkflows - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Module.GithubWorkflows" - + 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 ] - runs-on: ${{ matrix.job-runs-on }} + 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@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '10.0.x' - + dotnet-version: 10.0.x + - name: PackTool id: PackTool - run: dotnet run --project _atom/_atom.csproj PackTool --skip --headless + 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: Upload DecSm.Atom.Tool - uses: actions/upload-artifact@v4 + + - name: Store Invex.Atom.Tool + uses: actions/upload-artifact@v7 with: - name: DecSm.Atom.Tool-${{ matrix.job-runs-on }} - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Tool" - + 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 ] + 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 ] - runs-on: ${{ matrix.job-runs-on }} steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '10.0.x' - - uses: actions/setup-dotnet@v4 + dotnet-version: 10.0.x + + - name: Setup .NET 8.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '8.0.x' - - uses: actions/setup-dotnet@v4 + dotnet-version: 8.0.x + + - name: Setup .NET 9.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '9.0.x' - + dotnet-version: 9.0.x + - name: TestProjects id: TestProjects - run: dotnet run --project _atom/_atom.csproj TestProjects --skip --headless + 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: Upload DecSm.Atom.Tests - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }} - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Tests" - - - name: Upload DecSm.Atom.Analyzers.Tests - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }} - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Analyzers.Tests" - - - name: Upload DecSm.Atom.SourceGenerators.Tests - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }} - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.SourceGenerators.Tests" - - - name: Upload DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }} - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Upload DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }} - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Upload DecSm.Atom.Tool.Tests - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }} - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Tool.Tests" - + 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.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' + + PublishDocs: + permissions: + contents: write + needs: [ BuildDocs, 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 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@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Download DecSm.Atom - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom" - - - name: Download DecSm.Atom.Module.AzureKeyVault - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.AzureKeyVault - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.AzureKeyVault" - - - name: Download DecSm.Atom.Module.AzureStorage - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.AzureStorage - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.AzureStorage" - - - name: Download DecSm.Atom.Module.DevopsWorkflows - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows" - - - name: Download DecSm.Atom.Module.Dotnet - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.Dotnet - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.Dotnet" - - - name: Download DecSm.Atom.Module.GitVersion - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GitVersion - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GitVersion" - - - name: Download DecSm.Atom.Module.GithubWorkflows - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows" - - - name: Download DecSm.Atom.Tool - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool-windows-latest - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool" - - - name: Download DecSm.Atom.Tool - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool-windows-11-arm - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool" - - - name: Download DecSm.Atom.Tool - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool-ubuntu-latest - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool" - - - name: Download DecSm.Atom.Tool - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool-ubuntu-24.04-arm - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool" - - - name: Download DecSm.Atom.Tool - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool-macos-15-intel - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool" - - - name: Download DecSm.Atom.Tool - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool-macos-latest - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool" - + + - 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 + run: dotnet run --project _atom/_atom.csproj -- PushToNuget --skip --headless env: build-id: ${{ needs.SetupBuildInfo.outputs.build-id }} - azure-vault-app-secret: ${{ secrets.AZURE_VAULT_APP_SECRET }} - azure-vault-address: ${{ vars.AZURE_VAULT_ADDRESS }} - azure-vault-tenant-id: ${{ vars.AZURE_VAULT_TENANT_ID }} - azure-vault-app-id: ${{ vars.AZURE_VAULT_APP_ID }} - + nuget-push-api-key: ${{ secrets.NUGET_PUSH_API_KEY }} + PushToRelease: - needs: [ PackProjects, PackTool, TestProjects, SetupBuildInfo ] - runs-on: ubuntu-latest - if: contains(needs.SetupBuildInfo.outputs.build-version, '-') == false 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@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Download DecSm.Atom - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom" - - - name: Download DecSm.Atom.Module.AzureKeyVault - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.AzureKeyVault - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.AzureKeyVault" - - - name: Download DecSm.Atom.Module.AzureStorage - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.AzureStorage - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.AzureStorage" - - - name: Download DecSm.Atom.Module.DevopsWorkflows - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows" - - - name: Download DecSm.Atom.Module.Dotnet - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.Dotnet - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.Dotnet" - - - name: Download DecSm.Atom.Module.GitVersion - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GitVersion - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GitVersion" - - - name: Download DecSm.Atom.Module.GithubWorkflows - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows" - - - name: Download DecSm.Atom.Tool - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool-windows-latest - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool" - - - name: Download DecSm.Atom.Tool - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool-windows-11-arm - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool" - - - name: Download DecSm.Atom.Tool - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool-ubuntu-latest - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool" - - - name: Download DecSm.Atom.Tool - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool-ubuntu-24.04-arm - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool" - - - name: Download DecSm.Atom.Tool - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool-macos-15-intel - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool" - - - name: Download DecSm.Atom.Tool - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool-macos-latest - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-windows-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-windows-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-windows-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-windows-11-arm-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-windows-11-arm-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-windows-11-arm-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-ubuntu-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-ubuntu-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-ubuntu-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-ubuntu-24.04-arm-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-ubuntu-24.04-arm-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-ubuntu-24.04-arm-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-macos-15-intel-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-macos-15-intel-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-macos-15-intel-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-macos-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-macos-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tests-macos-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-windows-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-windows-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-windows-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-windows-11-arm-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-windows-11-arm-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-windows-11-arm-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-ubuntu-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-ubuntu-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-ubuntu-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-ubuntu-24.04-arm-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-ubuntu-24.04-arm-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-ubuntu-24.04-arm-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-macos-15-intel-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-macos-15-intel-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-macos-15-intel-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-macos-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-macos-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.Analyzers.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-macos-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Analyzers.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-windows-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-windows-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-windows-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-windows-11-arm-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-windows-11-arm-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-windows-11-arm-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-ubuntu-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-ubuntu-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-ubuntu-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-ubuntu-24.04-arm-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-ubuntu-24.04-arm-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-ubuntu-24.04-arm-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-macos-15-intel-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-macos-15-intel-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-macos-15-intel-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-macos-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-macos-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.SourceGenerators.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.SourceGenerators.Tests-macos-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.SourceGenerators.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-windows-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-windows-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-windows-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-windows-11-arm-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-windows-11-arm-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-windows-11-arm-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-ubuntu-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-ubuntu-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-ubuntu-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-ubuntu-24.04-arm-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-ubuntu-24.04-arm-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-ubuntu-24.04-arm-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-macos-15-intel-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-macos-15-intel-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-macos-15-intel-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-macos-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-macos-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-macos-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-windows-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-windows-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-windows-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-windows-11-arm-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-windows-11-arm-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-windows-11-arm-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-ubuntu-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-ubuntu-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-ubuntu-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-ubuntu-24.04-arm-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-ubuntu-24.04-arm-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-ubuntu-24.04-arm-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-macos-15-intel-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-macos-15-intel-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-macos-15-intel-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-macos-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-macos-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-macos-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-windows-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-windows-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-windows-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-windows-11-arm-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-windows-11-arm-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-windows-11-arm-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-ubuntu-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-ubuntu-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-ubuntu-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-ubuntu-24.04-arm-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-ubuntu-24.04-arm-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-ubuntu-24.04-arm-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-macos-15-intel-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-macos-15-intel-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-macos-15-intel-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-macos-latest-net8.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-macos-latest-net9.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - - - name: Download DecSm.Atom.Tool.Tests - uses: actions/download-artifact@v4 - with: - name: DecSm.Atom.Tool.Tests-macos-latest-net10.0 - path: "${{ github.workspace }}/.github/artifacts/DecSm.Atom.Tool.Tests" - + + - 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 + run: dotnet run --project _atom/_atom.csproj -- PushToRelease --skip --headless env: build-version: ${{ needs.SetupBuildInfo.outputs.build-version }} - azure-vault-app-secret: ${{ secrets.AZURE_VAULT_APP_SECRET }} - azure-vault-address: ${{ vars.AZURE_VAULT_ADDRESS }} - azure-vault-tenant-id: ${{ vars.AZURE_VAULT_TENANT_ID }} - azure-vault-app-id: ${{ vars.AZURE_VAULT_APP_ID }} github-token: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.github/workflows/Dependabot Enable auto-merge.yml b/.github/workflows/Dependabot Enable auto-merge.yml index 7a2cea74..d75849b7 100644 --- a/.github/workflows/Dependabot Enable auto-merge.yml +++ b/.github/workflows/Dependabot Enable auto-merge.yml @@ -1,33 +1,35 @@ name: Dependabot Enable auto-merge -permissions: { } on: pull_request: - branches: - - 'main' + branches: [ main ] + +permissions: { } jobs: - + ApproveDependabotPr: - runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + pull-requests: write if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-latest steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '10.0.x' - + dotnet-version: 10.0.x + - name: ApproveDependabotPr id: ApproveDependabotPr - run: dotnet run --project _atom/_atom.csproj ApproveDependabotPr --skip --headless + run: dotnet run --project _atom/_atom.csproj -- ApproveDependabotPr --skip --headless env: - azure-vault-app-secret: ${{ secrets.AZURE_VAULT_APP_SECRET }} - azure-vault-address: ${{ vars.AZURE_VAULT_ADDRESS }} - azure-vault-tenant-id: ${{ vars.AZURE_VAULT_TENANT_ID }} - azure-vault-app-id: ${{ vars.AZURE_VAULT_APP_ID }} - dependabot-enable-auto-merge-pat: ${{ secrets.DEPENDABOT_ENABLE_AUTO_MERGE_PAT }} pull-request-number: ${{ github.event.number }} + diff --git a/.github/workflows/Validate.yml b/.github/workflows/Validate.yml index 2f972bdb..bd8d7acf 100644 --- a/.github/workflows/Validate.yml +++ b/.github/workflows/Validate.yml @@ -1,14 +1,14 @@ name: Validate -permissions: { } on: - workflow_dispatch: pull_request: - branches: - - 'main' + branches: [ main ] + workflow_dispatch: + +permissions: { } jobs: - + SetupBuildInfo: runs-on: ubuntu-latest outputs: @@ -17,119 +17,155 @@ jobs: build-version: ${{ steps.SetupBuildInfo.outputs.build-version }} build-timestamp: ${{ steps.SetupBuildInfo.outputs.build-timestamp }} steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '10.0.x' - + dotnet-version: 10.0.x + - name: SetupBuildInfo id: SetupBuildInfo - run: dotnet run --project _atom/_atom.csproj SetupBuildInfo --skip --headless - + run: dotnet run --project _atom/_atom.csproj -- SetupBuildInfo --skip --headless + PackProjects: runs-on: ubuntu-latest steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '10.0.x' - + dotnet-version: 10.0.x + - name: PackProjects id: PackProjects - run: dotnet run --project _atom/_atom.csproj PackProjects --skip --headless - + run: dotnet run --project _atom/_atom.csproj -- PackProjects --skip --headless + 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 ] - runs-on: ${{ matrix.job-runs-on }} + 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@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '10.0.x' - + dotnet-version: 10.0.x + - name: PackTool id: PackTool - run: dotnet run --project _atom/_atom.csproj PackTool --skip --headless + run: dotnet run --project _atom/_atom.csproj -- PackTool --skip --headless env: job-runs-on: ${{ matrix.job-runs-on }} build-slice: ${{ matrix.job-runs-on }} - + 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 ] + 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 ] - runs-on: ${{ matrix.job-runs-on }} steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v4 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '10.0.x' - - uses: actions/setup-dotnet@v4 + dotnet-version: 10.0.x + + - name: Setup .NET 8.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '8.0.x' - - uses: actions/setup-dotnet@v4 + dotnet-version: 8.0.x + + - name: Setup .NET 9.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '9.0.x' - + dotnet-version: 9.0.x + - name: TestProjects id: TestProjects - run: dotnet run --project _atom/_atom.csproj TestProjects --skip --headless + 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: Upload DecSm.Atom.Tests - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }} - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Tests" - - - name: Upload DecSm.Atom.Analyzers.Tests - uses: actions/upload-artifact@v4 - with: - name: DecSm.Atom.Analyzers.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }} - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Analyzers.Tests" - - - name: Upload DecSm.Atom.SourceGenerators.Tests - uses: actions/upload-artifact@v4 + build-slice: '${{ matrix.job-runs-on }}-${{ matrix.test-framework }}' + + BuildDocs: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 with: - name: DecSm.Atom.SourceGenerators.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }} - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.SourceGenerators.Tests" - - - name: Upload DecSm.Atom.Module.DevopsWorkflows.Tests - uses: actions/upload-artifact@v4 + fetch-depth: 0 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 with: - name: DecSm.Atom.Module.DevopsWorkflows.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }} - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Module.DevopsWorkflows.Tests" - - - name: Upload DecSm.Atom.Module.GithubWorkflows.Tests - uses: actions/upload-artifact@v4 + dotnet-version: 10.0.x + + - name: BuildDocs + id: BuildDocs + run: dotnet run --project _atom/_atom.csproj -- BuildDocs --skip --headless + + CheckPrForBreakingChanges: + permissions: + checks: write + contents: write + id-token: write + pull-requests: write + needs: [ SetupBuildInfo ] + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 with: - name: DecSm.Atom.Module.GithubWorkflows.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }} - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Module.GithubWorkflows.Tests" - - - name: Upload DecSm.Atom.Tool.Tests - uses: actions/upload-artifact@v4 + fetch-depth: 0 + + - name: Setup .NET 10.0.x + uses: actions/setup-dotnet@v5 with: - name: DecSm.Atom.Tool.Tests-${{ matrix.job-runs-on }}-${{ matrix.test-framework }} - path: "${{ github.workspace }}/.github/publish/DecSm.Atom.Tool.Tests" + dotnet-version: 10.0.x + + - name: CheckPrForBreakingChanges + id: CheckPrForBreakingChanges + 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 }} + diff --git a/.gitignore b/.gitignore index f5c78594..e77afc22 100644 --- a/.gitignore +++ b/.gitignore @@ -403,6 +403,11 @@ FodyWeavers.xsd .vscode/ # Atom -!DecSm.Atom/Artifacts -!DecSm.Atom.Tests/Artifacts -atom-publish \ No newline at end of file +!src/Invex.Atom.Build/Artifacts +!src/Invex.Atom.Tests/Artifacts +atom-publish + +# DocFX +_site/ +api/*.yml +api/.manifest diff --git a/DecSm.Atom.Analyzers.Sample/DecSm.Atom.Analyzers.Sample.csproj b/DecSm.Atom.Analyzers.Sample/DecSm.Atom.Analyzers.Sample.csproj deleted file mode 100644 index a321f4e5..00000000 --- a/DecSm.Atom.Analyzers.Sample/DecSm.Atom.Analyzers.Sample.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net10.0 - false - - - - - - - - - diff --git a/DecSm.Atom.Analyzers/AnalyzerReleases.Unshipped.md b/DecSm.Atom.Analyzers/AnalyzerReleases.Unshipped.md deleted file mode 100644 index 44f7c8f4..00000000 --- a/DecSm.Atom.Analyzers/AnalyzerReleases.Unshipped.md +++ /dev/null @@ -1,4 +0,0 @@ -### New Rules - -| Rule ID | Category | Severity | Notes | -|---------|----------|----------|-------| \ No newline at end of file diff --git a/DecSm.Atom.Analyzers/Resources.resx b/DecSm.Atom.Analyzers/Resources.resx deleted file mode 100644 index a909a003..00000000 --- a/DecSm.Atom.Analyzers/Resources.resx +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, - PublicKeyToken=b77a5c561934e089 - - - - TargetDefinition.RequiresParam() should not directly reference a Param property. - An optional longer localizable description of the diagnostic. - - - Parameter '{0}' should be wrapped with nameof operator - The format-able message the diagnostic displays. - - - RequiresParam should use nameof expression - The title of the diagnostic. - - - Replace with nameof({0}) - The title of the code fix. - - \ No newline at end of file diff --git a/DecSm.Atom.DotnetCliGenerator/_usings.cs b/DecSm.Atom.DotnetCliGenerator/_usings.cs deleted file mode 100644 index a4a59a5f..00000000 --- a/DecSm.Atom.DotnetCliGenerator/_usings.cs +++ /dev/null @@ -1,7 +0,0 @@ -global using System.Text; -global using System.Text.Json; -global using System.Text.RegularExpressions; -global using DecSm.Atom.Build.Definition; -global using DecSm.Atom.Hosting; -global using DecSm.Atom.Util.Scope; -global using JetBrains.Annotations; diff --git a/DecSm.Atom.Module.AzureKeyVault/DecSm.Atom.Module.AzureKeyVault.props b/DecSm.Atom.Module.AzureKeyVault/DecSm.Atom.Module.AzureKeyVault.props deleted file mode 100644 index 6a639cd0..00000000 --- a/DecSm.Atom.Module.AzureKeyVault/DecSm.Atom.Module.AzureKeyVault.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/DecSm.Atom.Module.AzureKeyVault/UseAzureKeyVault.cs b/DecSm.Atom.Module.AzureKeyVault/UseAzureKeyVault.cs deleted file mode 100644 index 5e7b5795..00000000 --- a/DecSm.Atom.Module.AzureKeyVault/UseAzureKeyVault.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace DecSm.Atom.Module.AzureKeyVault; - -/// -/// Represents a workflow option to enable or disable the use of Azure Key Vault. -/// -/// -/// When this option is enabled, the build process will attempt to connect to Azure Key Vault -/// using the configured parameters to retrieve secrets. -/// -[PublicAPI] -public sealed record UseAzureKeyVault : ToggleWorkflowOption; diff --git a/DecSm.Atom.Module.AzureKeyVault/_usings.cs b/DecSm.Atom.Module.AzureKeyVault/_usings.cs deleted file mode 100644 index 8da2d087..00000000 --- a/DecSm.Atom.Module.AzureKeyVault/_usings.cs +++ /dev/null @@ -1,15 +0,0 @@ -global using Azure.Core; -global using Azure.Identity; -global using Azure.Security.KeyVault.Secrets; -global using DecSm.Atom.Args; -global using DecSm.Atom.Build; -global using DecSm.Atom.Build.Definition; -global using DecSm.Atom.Hosting; -global using DecSm.Atom.Params; -global using DecSm.Atom.Secrets; -global using DecSm.Atom.Workflows; -global using DecSm.Atom.Workflows.Definition.Options; -global using JetBrains.Annotations; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Hosting; -global using Microsoft.Extensions.Logging; diff --git a/DecSm.Atom.Module.AzureStorage/DecSm.Atom.Module.AzureStorage.props b/DecSm.Atom.Module.AzureStorage/DecSm.Atom.Module.AzureStorage.props deleted file mode 100644 index cd8adf5e..00000000 --- a/DecSm.Atom.Module.AzureStorage/DecSm.Atom.Module.AzureStorage.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/CheckoutOptionBuild.cs b/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/CheckoutOptionBuild.cs deleted file mode 100644 index d50aff22..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/CheckoutOptionBuild.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Tests.Workflows; - -[BuildDefinition] -public partial class CheckoutOptionBuild : MinimalBuildDefinition, IDevopsWorkflows, ICheckoutOptionTarget -{ - public override IReadOnlyList Workflows => - [ - new("checkoutoption-workflow") - { - Triggers = [ManualTrigger.Empty], - Targets = - [ - WorkflowTargets.CheckoutOptionTarget.WithOptions( - DevopsCheckoutOption.Create(new(true, "recursive"))), - ], - WorkflowTypes = [Devops.WorkflowType], - }, - ]; -} - -public interface ICheckoutOptionTarget -{ - Target CheckoutOptionTarget => t => t; -} diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/EnvironmentBuild.cs b/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/EnvironmentBuild.cs deleted file mode 100644 index b6901faf..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/EnvironmentBuild.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Tests.Workflows; - -[BuildDefinition] -public partial class EnvironmentBuild : MinimalBuildDefinition, IDevopsWorkflows, IEnvironmentTarget -{ - public override IReadOnlyList Workflows => - [ - new("environment-workflow") - { - Triggers = [ManualTrigger.Empty], - Targets = [WorkflowTargets.EnvironmentTarget.WithOptions(DeployToEnvironment.Create("test-env-1"))], - WorkflowTypes = [Devops.WorkflowType], - }, - ]; -} - -public interface IEnvironmentTarget -{ - Target EnvironmentTarget => t => t; -} diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/MinimalBuild.cs b/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/MinimalBuild.cs deleted file mode 100644 index ca9e0a65..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/MinimalBuild.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Tests.Workflows; - -[BuildDefinition] -public partial class MinimalBuild : MinimalBuildDefinition; diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/SetupDotnetBuild.cs b/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/SetupDotnetBuild.cs deleted file mode 100644 index 1a938330..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/SetupDotnetBuild.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Tests.Workflows; - -[BuildDefinition] -public partial class SetupDotnetBuild : MinimalBuildDefinition, IDevopsWorkflows, ISetupDotnetTarget -{ - public override IReadOnlyList Workflows => - [ - new("setup-dotnet") - { - Triggers = [GitPushTrigger.ToMain], - Targets = [WorkflowTargets.SetupDotnetTarget.WithOptions(new SetupDotnetStep("9.0.x"))], - WorkflowTypes = [Devops.WorkflowType], - }, - ]; -} - -public interface ISetupDotnetTarget -{ - Target SetupDotnetTarget => t => t; -} diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.CustomArtifactBuild_GeneratesWorkflow.verified.txt b/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.CustomArtifactBuild_GeneratesWorkflow.verified.txt deleted file mode 100644 index e584322a..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.CustomArtifactBuild_GeneratesWorkflow.verified.txt +++ /dev/null @@ -1,116 +0,0 @@ -name: custom-artifact-workflow - -trigger: none - -jobs: - - - job: SetupBuildInfo - pool: - vmImage: ubuntu-latest - steps: - - - checkout: self - fetchDepth: 0 - - - script: dotnet run --project AtomTest/AtomTest.csproj SetupBuildInfo --skip --headless - name: SetupBuildInfo - - - job: ArtifactTarget1 - dependsOn: [ SetupBuildInfo ] - pool: - vmImage: ubuntu-latest - variables: - build-name: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-name'] ] - build-id: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-id'] ] - steps: - - - checkout: self - fetchDepth: 0 - - - script: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget1 --skip --headless - name: ArtifactTarget1 - - - script: dotnet run --project AtomTest/AtomTest.csproj StoreArtifact --skip --headless - env: - build-name: $(build-name) - build-id: $(build-id) - atom-artifacts: TestArtifact1 - - - job: ArtifactTarget2 - dependsOn: [ ArtifactTarget1, SetupBuildInfo ] - pool: - vmImage: ubuntu-latest - variables: - build-name: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-name'] ] - build-id: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-id'] ] - steps: - - - checkout: self - fetchDepth: 0 - - - script: dotnet run --project AtomTest/AtomTest.csproj RetrieveArtifact --skip --headless - env: - build-name: $(build-name) - build-id: $(build-id) - atom-artifacts: TestArtifact1 - - - script: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget2 --skip --headless - name: ArtifactTarget2 - - - script: dotnet run --project AtomTest/AtomTest.csproj StoreArtifact --skip --headless - env: - build-name: $(build-name) - build-id: $(build-id) - atom-artifacts: TestArtifact2,TestArtifact2 - - - job: ArtifactTarget3 - dependsOn: [ ArtifactTarget1, ArtifactTarget2, SetupBuildInfo ] - pool: - vmImage: ubuntu-latest - variables: - build-name: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-name'] ] - build-id: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-id'] ] - steps: - - - checkout: self - fetchDepth: 0 - - - script: dotnet run --project AtomTest/AtomTest.csproj RetrieveArtifact --skip --headless - env: - build-name: $(build-name) - build-id: $(build-id) - atom-artifacts: TestArtifact1,TestArtifact2,TestArtifact2 - - - script: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget3 --skip --headless - name: ArtifactTarget3 - - - job: ArtifactTarget4 - dependsOn: [ ArtifactTarget2, SetupBuildInfo ] - strategy: - matrix: - 001_Slice1: - slice: 'Slice1' - 002_Slice2: - slice: 'Slice2' - pool: - vmImage: ubuntu-latest - variables: - build-name: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-name'] ] - build-id: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-id'] ] - steps: - - - checkout: self - fetchDepth: 0 - - - script: dotnet run --project AtomTest/AtomTest.csproj RetrieveArtifact --skip --headless - env: - build-name: $(build-name) - build-id: $(build-id) - atom-artifacts: TestArtifact2 - build-slice: $(slice) - - - script: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget4 --skip --headless - name: ArtifactTarget4 - env: - slice: $(slice) - build-slice: $(slice) diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.EnvironmentBuild_GeneratesWorkflow.verified.txt b/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.EnvironmentBuild_GeneratesWorkflow.verified.txt deleted file mode 100644 index 11f7a599..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.EnvironmentBuild_GeneratesWorkflow.verified.txt +++ /dev/null @@ -1,17 +0,0 @@ -name: environment-workflow - - -jobs: - - - job: EnvironmentTarget - pool: - vmImage: ubuntu-latest - environment: - name: test-env-1 - steps: - - - checkout: self - fetchDepth: 0 - - - script: dotnet run --project AtomTest/AtomTest.csproj EnvironmentTarget --skip --headless - name: EnvironmentTarget diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/_usings.cs b/DecSm.Atom.Module.DevopsWorkflows.Tests/_usings.cs deleted file mode 100644 index 654c9d68..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/_usings.cs +++ /dev/null @@ -1,17 +0,0 @@ -global using DecSm.Atom.Args; -global using DecSm.Atom.Artifacts; -global using DecSm.Atom.Build; -global using DecSm.Atom.Build.Definition; -global using DecSm.Atom.Hosting; -global using DecSm.Atom.Params; -global using DecSm.Atom.TestUtils; -global using DecSm.Atom.Workflows.Definition; -global using DecSm.Atom.Workflows.Definition.Options; -global using DecSm.Atom.Workflows.Definition.Triggers; -global using DecSm.Atom.Workflows.Options; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Hosting; -global using Shouldly; -global using Spectre.Console.Testing; -global using static DecSm.Atom.TestUtils.TestUtils; -global using Target = DecSm.Atom.Build.Definition.Target; diff --git a/DecSm.Atom.Module.DevopsWorkflows/DecSm.Atom.Module.DevopsWorkflows.props b/DecSm.Atom.Module.DevopsWorkflows/DecSm.Atom.Module.DevopsWorkflows.props deleted file mode 100644 index 709e4c1f..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows/DecSm.Atom.Module.DevopsWorkflows.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/DecSm.Atom.Module.DevopsWorkflows/DevopsCheckoutOption.cs b/DecSm.Atom.Module.DevopsWorkflows/DevopsCheckoutOption.cs deleted file mode 100644 index 7f130027..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows/DevopsCheckoutOption.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace DecSm.Atom.Module.DevopsWorkflows; - -/// -/// Represents a workflow option for configuring the checkout step in Azure DevOps Pipelines. -/// -/// -/// This option allows customization of the checkout action, such as enabling LFS and handling submodules. -/// -[PublicAPI] -public sealed record - DevopsCheckoutOption : WorkflowOption -{ - /// - public override bool AllowMultiple => false; - - /// - /// Defines the configurable values for the checkout step. - /// - /// Whether to enable Git LFS support. - /// How to handle submodules (e.g., "true", "recursive", "false"). - [PublicAPI] - public sealed record DevopsCheckoutOptionValues(bool Lfs = false, string? Submodules = null); -} diff --git a/DecSm.Atom.Module.DevopsWorkflows/Extensions.cs b/DecSm.Atom.Module.DevopsWorkflows/Extensions.cs deleted file mode 100644 index e9817e18..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows/Extensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace DecSm.Atom.Module.DevopsWorkflows; - -/// -/// Provides extension methods for to simplify Azure DevOps workflow -/// configuration. -/// -[PublicAPI] -public static class Extensions -{ - /// - /// Configures the workflow target to run on a matrix of Azure DevOps agent pool labels. - /// - /// The workflow target definition to extend. - /// An array of agent pool labels (e.g., "ubuntu-latest", "windows-latest"). - /// The modified for chaining. - /// - /// This method sets up a matrix dimension for the `JobRunsOn` parameter, allowing the job - /// to execute on multiple agent pool environments. It also adds the - /// option to indicate that the agent pool is determined by the matrix. - /// - public static WorkflowTargetDefinition WithDevopsPoolMatrix( - this WorkflowTargetDefinition workflowTargetDefinition, - string[] labels) => - workflowTargetDefinition - .WithMatrixDimensions(new MatrixDimension(nameof(IJobRunsOn.JobRunsOn)) - { - Values = labels, - }) - .WithOptions(DevopsPool.SetByMatrix); -} diff --git a/DecSm.Atom.Module.DevopsWorkflows/Generation/DevopsWorkflowType.cs b/DecSm.Atom.Module.DevopsWorkflows/Generation/DevopsWorkflowType.cs deleted file mode 100644 index 3dcc4080..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows/Generation/DevopsWorkflowType.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Generation; - -[PublicAPI] -public sealed record DevopsWorkflowType : IWorkflowType -{ - public bool IsRunning => Devops.IsDevopsPipelines; -} diff --git a/DecSm.Atom.Module.DevopsWorkflows/Generation/DevopsWorkflowWriter.cs b/DecSm.Atom.Module.DevopsWorkflows/Generation/DevopsWorkflowWriter.cs deleted file mode 100644 index c5b2df49..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows/Generation/DevopsWorkflowWriter.cs +++ /dev/null @@ -1,758 +0,0 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Generation; - -internal sealed partial class DevopsWorkflowWriter( - IAtomFileSystem fileSystem, - IBuildDefinition buildDefinition, - BuildModel buildModel, - ILogger logger -) : WorkflowFileWriter(fileSystem, logger) -{ - private readonly IAtomFileSystem _fileSystem = fileSystem; - - protected override string FileExtension => "yml"; - - protected override int TabSize => 2; - - protected override RootedPath FileLocation => _fileSystem.AtomRootDirectory / ".devops" / "workflows"; - - [GeneratedRegex("-+")] - private static partial Regex HyphenReductionRegex(); - - protected override void WriteWorkflow(WorkflowModel workflow) - { - WriteLine($"name: {workflow.Name}"); - WriteLine(); - - var manualTrigger = workflow - .Triggers - .OfType() - .FirstOrDefault(); - - if (manualTrigger is { Inputs.Count: > 0 }) - using (WriteSection("parameters:")) - { - foreach (var input in manualTrigger.Inputs) - using (WriteSection($"- name: {input.Name}")) - { - WriteLine($"displayName: '{input.Name} | {input.Description}'"); - - switch (input) - { - case ManualBoolInput boolInput: - - WriteLine("type: boolean"); - - if (boolInput.DefaultValue is not null) - WriteLine($"default: '{boolInput.DefaultValue.Value}'"); - - break; - - case ManualStringInput stringInput: - - WriteLine("type: string"); - - if (stringInput.DefaultValue is not null) - WriteLine($"default: '{stringInput.DefaultValue}'"); - - break; - - case ManualChoiceInput choiceInput: - - WriteLine("type: string"); - - WriteLine(choiceInput.DefaultValue is not null - ? $"default: {choiceInput.DefaultValue}" - : $"default: '{choiceInput.Choices[0]}'"); - - using (WriteSection("values:")) - { - foreach (var choice in choiceInput.Choices) - WriteLine($"- '{choice}'"); - } - - break; - } - } - } - - // TODO: Variables - - var variableGroups = workflow - .Options - .OfType() - .ToArray(); - - if (variableGroups.Length > 0) - using (WriteSection("variables:")) - { - foreach (var variableGroup in variableGroups) - WriteLine($"- group: {variableGroup.Name}"); - } - - var pushTriggers = workflow - .Triggers - .OfType() - .ToArray(); - - if (pushTriggers.Length > 0) - using (WriteSection("trigger:")) - { - foreach (var pushTrigger in pushTriggers) - { - using (WriteSection("branches:")) - { - if (pushTrigger.IncludedBranches.Count > 0) - using (WriteSection("include:")) - { - foreach (var branch in pushTrigger.IncludedBranches) - WriteLine($"- '{branch}'"); - } - - if (pushTrigger.ExcludedBranches.Count > 0) - using (WriteSection("exclude:")) - { - foreach (var branch in pushTrigger.ExcludedBranches) - WriteLine($"- '{branch}'"); - } - } - - if (pushTrigger.IncludedPaths.Count > 0 || pushTrigger.ExcludedPaths.Count > 0) - using (WriteSection("paths:")) - { - if (pushTrigger.IncludedPaths.Count > 0) - using (WriteSection("include:")) - { - foreach (var path in pushTrigger.IncludedPaths) - WriteLine($"- '{path}'"); - } - - if (pushTrigger.ExcludedPaths.Count > 0) - using (WriteSection("exclude:")) - { - foreach (var path in pushTrigger.ExcludedPaths) - WriteLine($"- '{path}'"); - } - } - - // ReSharper disable once InvertIf - if (pushTrigger.IncludedTags.Count > 0 || pushTrigger.ExcludedTags.Count > 0) - using (WriteSection("tags:")) - { - if (pushTrigger.IncludedTags.Count > 0) - using (WriteSection("include:")) - { - foreach (var tag in pushTrigger.IncludedTags) - WriteLine($"- '{tag}'"); - } - - // ReSharper disable once InvertIf - if (pushTrigger.ExcludedTags.Count > 0) - using (WriteSection("exclude:")) - { - foreach (var tag in pushTrigger.ExcludedTags) - WriteLine($"- '{tag}'"); - } - } - } - } - - if (manualTrigger is null && pushTriggers.Length is 0) - WriteLine("trigger: none"); - - WriteLine(); - - using (WriteSection("jobs:")) - { - foreach (var job in workflow.Jobs) - { - WriteLine(); - WriteJob(workflow, job); - } - } - } - - private void WriteJob(WorkflowModel workflow, WorkflowJobModel job) - { - using (WriteSection($"- job: {job.Name}")) - { - var jobRequirementNames = job.JobDependencies; - - if (jobRequirementNames.Count > 0) - WriteLine($"dependsOn: [ {string.Join(", ", jobRequirementNames)} ]"); - - if (job.MatrixDimensions.Count > 0) - using (WriteSection("strategy:")) - using (WriteSection("matrix:")) - { - var dimensions = job - .MatrixDimensions - .Select(d => new MatrixDimension(d.Name) - { - Values = d.Values, - }) - .ToArray(); - - var dimensionValues = dimensions - .Select(d => d.Values) - .ToArray(); - - var dimensionNames = dimensions - .Select(d => d.Name) - .ToArray(); - - var dimensionValueCounts = dimensionValues - .Select(d => d.Count) - .ToArray(); - - var dimensionValueIndices = new int[dimensionValues.Length]; - - var counter = 1; - - while (true) - { - // Compute the current dimension values based on the indices - var currentDimensionValues = dimensionValueIndices - .Select((index, i) => dimensionValues[i][index]) - - // Replace any invalid characters in the dimension value with a hyphen - .Select(value => new string(value - .Select(c => char.IsLetterOrDigit(c) - ? c - : '-') - .ToArray())) - - // Replace multiple hyphens with a single hyphen - .Select(value => HyphenReductionRegex() - .Replace(value, "-")) - - // Trim hyphens from the start and end of the dimension value - .Select(value => value.Trim('-')) - .ToArray(); - - var dimensionValueName = $"{counter++:D3}_{string.Join("_", currentDimensionValues)}"; - - using (WriteSection($"{dimensionValueName}:")) - { - for (var i = 0; i < dimensions.Length; i++) - WriteLine( - $"{buildDefinition.ParamDefinitions[dimensionNames[i]].ArgName}: '{dimensionValues[i][dimensionValueIndices[i]]}'"); - } - - var dimensionIndex = 0; - - while (dimensionIndex < dimensionValues.Length) - { - if (dimensionValueIndices[dimensionIndex] < dimensionValueCounts[dimensionIndex] - 1) - { - dimensionValueIndices[dimensionIndex]++; - - break; - } - - dimensionValueIndices[dimensionIndex] = 0; - dimensionIndex++; - } - - if (dimensionIndex == dimensionValues.Length) - break; - } - } - - var poolOption = job - .Options - .Concat(workflow.Options) - .OfType() - .FirstOrDefault() ?? - DevopsPool.UbuntuLatest; - - using (WriteSection("pool:")) - { - if (poolOption.Hosted is { Length: > 0 }) - WriteLine($"vmImage: {poolOption.Hosted}"); - else if (poolOption.Name is { Length: > 0 }) - WriteLine($"name: {poolOption.Name}"); - - if (poolOption.Demands.Count > 0) - using (WriteSection("demands:")) - { - foreach (var demand in poolOption.Demands) - WriteLine($"- {demand}"); - } - } - - var environmentOption = job - .Options - .Concat(workflow.Options) - .OfType() - .FirstOrDefault(); - - if (environmentOption is not null) - using (WriteSection("environment:")) - WriteLine($"name: {environmentOption.Value}"); - - var variables = new Dictionary(); - - var targetsForConsumedVariableDeclaration = new List(); - - foreach (var commandStep in job.Steps) - { - var target = buildModel.GetTarget(commandStep.Name); - targetsForConsumedVariableDeclaration.Add(target); - - if (!UseCustomArtifactProvider.IsEnabled(workflow.Options) || commandStep.SuppressArtifactPublishing) - continue; - - if (target.ConsumedArtifacts.Count > 0) - targetsForConsumedVariableDeclaration.Add( - buildModel.GetTarget(nameof(IRetrieveArtifact.RetrieveArtifact))); - - if (target.ProducedArtifacts.Count > 0) - targetsForConsumedVariableDeclaration.Add( - buildModel.GetTarget(nameof(IStoreArtifact.StoreArtifact))); - } - - foreach (var consumedVariable in targetsForConsumedVariableDeclaration.SelectMany(x => x.ConsumedVariables)) - { - var variableName = buildDefinition.ParamDefinitions[consumedVariable.VariableName].ArgName; - - variables[variableName] = - $"$[ dependencies.{consumedVariable.TargetName}.outputs['{consumedVariable.TargetName}.{variableName}'] ]"; - } - - if (variables.Count > 0) - using (WriteSection("variables:")) - { - foreach (var (name, value) in variables) - WriteLine($"{name}: {value}"); - } - - using (WriteSection("steps:")) - { - foreach (var step in job.Steps) - { - WriteLine(); - - WriteStep(workflow, step, job); - } - } - } - } - - private void WriteStep(WorkflowModel workflow, WorkflowStepModel step, WorkflowJobModel job) - { - if (workflow - .Options - .Concat(step.Options) - .OfType() - .FirstOrDefault() is { Value: not null } checkoutOption) - using (WriteSection("- checkout: self")) - { - WriteLine("fetchDepth: 0"); - - if (checkoutOption.Value.Lfs) - WriteLine("lfs: true"); - - if (!string.IsNullOrWhiteSpace(checkoutOption.Value.Submodules)) - WriteLine($"submodules: {checkoutOption.Value.Submodules}"); - } - else - using (WriteSection("- checkout: self")) - WriteLine("fetchDepth: 0"); - - WriteLine(); - - var commandStepTarget = buildModel.GetTarget(step.Name); - - var matrixParams = job - .MatrixDimensions - .Select(dimension => buildDefinition.ParamDefinitions[dimension.Name].ArgName) - .Select(name => (Name: name, Value: $"$({name})")) - .ToArray(); - - var buildSlice = (Name: "build-slice", Value: string.Join("-", matrixParams.Select(x => x.Value))); - - if (!string.IsNullOrWhiteSpace(buildSlice.Value)) - matrixParams = matrixParams - .Append(buildSlice) - .ToArray(); - - var setupDotnetSteps = workflow - .Options - .Concat(step.Options) - .OfType() - .ToList(); - - if (setupDotnetSteps.Count > 0) - foreach (var setupDotnetStep in setupDotnetSteps) - using (WriteSection("- task: UseDotNet@2")) - { - if (setupDotnetStep.DotnetVersion is not { Length: > 0 }) - continue; - - using (WriteSection("inputs:")) - { - WriteLine($"version: '{setupDotnetStep.DotnetVersion}'\n"); - - if (setupDotnetStep.Quality is not null) - WriteLine("includePreviewVersions: 'true'"); - } - } - - var setupNugetSteps = workflow - .Options - .Concat(step.Options) - .OfType() - .ToList(); - - if (setupNugetSteps.Count > 0) - { - var feedsToAdd = setupNugetSteps - .SelectMany(x => x.FeedsToAdd) - .DistinctBy(x => x.FeedName) - .ToList(); - - // If we know the SetupDotnet step was run for dotnet 10+, - // then we can use the dotnet tool exec command instead of installing the tool to run it - if (setupDotnetSteps.Any(x => - SemVer.TryParse(x.DotnetVersion?.Replace("x", "0"), out var version) && version.Major >= 10)) - { - using (WriteSection("- script: |")) - { - foreach (var feedToAdd in feedsToAdd) - WriteLine( - $"dotnet tool exec decsm.atom.tool -y -- nuget-add --name \"{feedToAdd.FeedName}\" --url \"{feedToAdd.FeedUrl}\""); - - WriteLine("displayName: 'Setup NuGet'"); - - using (WriteSection("env:")) - { - foreach (var feedToAdd in feedsToAdd) - WriteLine( - $"{AddNugetFeedsStep.GetEnvVarNameForFeed(feedToAdd.FeedName)}: $({feedToAdd.SecretName})"); - } - - WriteLine(); - } - } - else - { - using (WriteSection("- script: dotnet tool update --global DecSm.Atom.Tool")) - WriteLine("displayName: 'Install atom tool'"); - - WriteLine(); - - using (WriteSection("- script: |")) - { - foreach (var feedToAdd in feedsToAdd) - WriteLine($" atom nuget-add --name \"{feedToAdd.FeedName}\" --url \"{feedToAdd.FeedUrl}\""); - - WriteLine("displayName: 'Setup NuGet'"); - - using (WriteSection("env:")) - { - foreach (var feedToAdd in feedsToAdd) - WriteLine( - $"{AddNugetFeedsStep.GetEnvVarNameForFeed(feedToAdd.FeedName)}: $({feedToAdd.SecretName})"); - } - - WriteLine(); - } - } - } - - if (commandStepTarget.ConsumedArtifacts.Count > 0) - { - foreach (var consumedArtifact in commandStepTarget.ConsumedArtifacts) - if (workflow - .Jobs - .SelectMany(x => x.Steps) - .Single(x => x.Name == consumedArtifact.TargetName) - .SuppressArtifactPublishing) - logger.LogWarning( - "Workflow {WorkflowName} command {CommandName} consumes artifact {ArtifactName} from target {SourceTargetName}, which has artifact publishing suppressed; this may cause the workflow to fail", - workflow.Name, - step.Name, - consumedArtifact.ArtifactName, - consumedArtifact.TargetName); - - if (UseCustomArtifactProvider.IsEnabled(workflow.Options)) - { - WriteCommandStep(workflow, - new(nameof(IRetrieveArtifact.RetrieveArtifact)), - buildModel.GetTarget(nameof(IRetrieveArtifact.RetrieveArtifact)), - [ - ("atom-artifacts", - string.Join(",", commandStepTarget.ConsumedArtifacts.Select(x => x.ArtifactName))), - !string.IsNullOrWhiteSpace(buildSlice.Value) - ? buildSlice - : default, - ], - false); - - WriteLine(); - } - else - { - foreach (var artifact in commandStepTarget.ConsumedArtifacts) - { - using (WriteSection("- task: DownloadPipelineArtifact@2")) - { - WriteLine($"displayName: {artifact.ArtifactName}"); - - using (WriteSection("inputs:")) - { - WriteLine(artifact.BuildSlice is { Length: > 0 } - ? $"artifact: {artifact.ArtifactName}-{artifact.BuildSlice}" - : !string.IsNullOrWhiteSpace(buildSlice.Value) - ? $"artifact: {artifact.ArtifactName}-{buildSlice.Value}" - : $"artifact: {artifact.ArtifactName}"); - - WriteLine($"path: \"{Devops.PipelineArtifactDirectory}/{artifact.ArtifactName}\""); - } - } - - WriteLine(); - } - } - } - - WriteCommandStep(workflow, step, commandStepTarget, matrixParams, true); - - // ReSharper disable once InvertIf - if (commandStepTarget.ProducedArtifacts.Count > 0 && !step.SuppressArtifactPublishing) - { - if (UseCustomArtifactProvider.IsEnabled(workflow.Options)) - { - WriteLine(); - - WriteCommandStep(workflow, - new(nameof(IStoreArtifact.StoreArtifact)), - buildModel.GetTarget(nameof(IStoreArtifact.StoreArtifact)), - [ - ("atom-artifacts", - string.Join(",", commandStepTarget.ProducedArtifacts.Select(x => x.ArtifactName))), - !string.IsNullOrWhiteSpace(buildSlice.Value) - ? buildSlice - : default, - ], - false); - } - else - { - if (commandStepTarget.ProducedArtifacts.Count > 0) - WriteLine(); - - foreach (var artifact in commandStepTarget.ProducedArtifacts) - { - using (WriteSection("- task: PublishPipelineArtifact@1")) - { - WriteLine($"displayName: {artifact.ArtifactName}"); - - using (WriteSection("inputs:")) - { - WriteLine(artifact.BuildSlice is { Length: > 0 } - ? $"artifactName: {artifact.ArtifactName}-{artifact.BuildSlice}" - : !string.IsNullOrWhiteSpace(buildSlice.Value) - ? $"artifactName: {artifact.ArtifactName}-{buildSlice.Value}" - : $"artifactName: {artifact.ArtifactName}"); - - WriteLine($"targetPath: \"{Devops.PipelinePublishDirectory}/{artifact.ArtifactName}\""); - } - } - - WriteLine(); - } - } - } - } - - private void WriteCommandStep( - WorkflowModel workflow, - WorkflowStepModel workflowStep, - TargetModel target, - (string name, string value)[] extraParams, - bool includeName) - { - string runScript; - - if (_fileSystem.IsFileBasedApp) - { - if (AppContext.GetData("EntryPointFilePath") is not string fileName) - throw new InvalidOperationException("EntryPointFilePath is null"); - - var filePathRelativeToRoot = - _fileSystem.FileSystem.Path.GetRelativePath(_fileSystem.AtomRootDirectory, fileName); - - runScript = $"- script: dotnet run --file {filePathRelativeToRoot} {workflowStep.Name} --skip --headless"; - } - else - { - var projectPath = FindProjectPath(_fileSystem, _fileSystem.ProjectName); - - runScript = $"- script: dotnet run --project {projectPath} {workflowStep.Name} --skip --headless"; - } - - using (WriteSection(runScript)) - { - if (includeName) - WriteLine($"name: {workflowStep.Name}"); - - var env = new Dictionary(); - - foreach (var githubManualTrigger in workflow.Triggers.OfType()) - { - if (githubManualTrigger.Inputs is null or []) - continue; - - foreach (var input in githubManualTrigger.Inputs.Where(i => target - .Params - .Select(p => p.Param.ArgName) - .Any(p => p == i.Name))) - env[input.Name] = $"${{{{ parameters.{input.Name} }}}}"; - } - - foreach (var consumedVariable in target.ConsumedVariables) - env[buildDefinition.ParamDefinitions[consumedVariable.VariableName].ArgName] = - $"$({buildDefinition.ParamDefinitions[consumedVariable.VariableName].ArgName})"; - - var requiredSecrets = target - .Params - .Where(x => x.Param.IsSecret) - .Select(x => x) - .ToArray(); - - if (requiredSecrets.Any(x => x.Param.IsSecret)) - { - foreach (var injectedSecret in workflow.Options.OfType()) - { - if (injectedSecret.Value is null) - { - logger.LogWarning( - "Workflow {WorkflowName} command {CommandName} has a secret injection with a null value", - workflow.Name, - workflowStep.Name); - - continue; - } - - var paramDefinition = buildDefinition.ParamDefinitions.GetValueOrDefault(injectedSecret.Value); - - if (paramDefinition is not null) - env[paramDefinition.ArgName] = $"$({paramDefinition.ArgName.ToUpper().Replace('-', '_')})"; - } - - foreach (var injectedEnvVar in workflow.Options.OfType()) - { - if (injectedEnvVar.Value is null) - { - logger.LogWarning( - "Workflow {WorkflowName} command {CommandName} has a secret environment variable injection with a null value", - workflow.Name, - workflowStep.Name); - - continue; - } - - var paramDefinition = buildDefinition.ParamDefinitions.GetValueOrDefault(injectedEnvVar.Value); - - if (paramDefinition is not null) - env[paramDefinition.ArgName] = $"$({paramDefinition.ArgName.ToUpper().Replace('-', '_')})"; - } - } - - foreach (var requiredSecret in requiredSecrets) - { - var injectedSecret = workflow - .Options - .Concat(workflowStep.Options) - .OfType() - .FirstOrDefault(x => x.Value == requiredSecret.Param.Name); - - if (injectedSecret is not null) - env[requiredSecret.Param.ArgName] = - $"$({requiredSecret.Param.ArgName.ToUpper().Replace('-', '_')})"; - } - - var environmentInjections = workflow.Options.OfType(); - var paramInjections = workflow.Options.OfType(); - environmentInjections = environmentInjections.Where(e => paramInjections.All(p => p.Name != e.Value)); - - foreach (var environmentInjection in environmentInjections.Where(e => e.Value is not null)) - { - if (!buildDefinition.ParamDefinitions.TryGetValue(environmentInjection.Value!, out var paramDefinition)) - { - logger.LogWarning( - "Workflow {WorkflowName} command {CommandName} has an injection for parameter {ParamName} that does not exist", - workflow.Name, - workflowStep.Name, - environmentInjection.Value); - - continue; - } - - env[paramDefinition.ArgName] = $"$({paramDefinition.ArgName.ToUpper().Replace('-', '_')})"; - } - - foreach (var paramInjection in paramInjections) - { - if (!buildDefinition.ParamDefinitions.TryGetValue(paramInjection.Name, out var paramDefinition)) - { - logger.LogWarning( - "Workflow {WorkflowName} command {CommandName} has an injection for parameter {ParamName} that does not exist", - workflow.Name, - workflowStep.Name, - paramInjection.Name); - - continue; - } - - env[paramDefinition.ArgName] = paramInjection.Value; - } - - var validEnv = env - .Where(static x => x.Value is { Length: > 0 }) - .ToList(); - - var validExtraParams = extraParams - .Where(static x => x.value is { Length: > 0 }) - .ToList(); - - // ReSharper disable once InvertIf - if (validEnv.Count > 0 || validExtraParams.Count > 0) - using (WriteSection("env:")) - { - foreach (var (key, value) in validEnv) - WriteLine($"{key}: {value}"); - - foreach (var (key, value) in validExtraParams) - WriteLine($"{key}: {value}"); - } - } - } - - private static string FindProjectPath(IAtomFileSystem fileSystem, string projectName) - { - var projectPath = fileSystem - .FileSystem - .DirectoryInfo - .New(fileSystem.AtomRootDirectory) - .EnumerateFiles("*.csproj", - new EnumerationOptions - { - IgnoreInaccessible = true, - MaxRecursionDepth = 4, - RecurseSubdirectories = true, - ReturnSpecialDirectories = false, - }) - .FirstOrDefault(f => f.Name.Equals($"{projectName}.csproj", StringComparison.OrdinalIgnoreCase)); - - if (projectPath?.FullName is null) - throw new InvalidOperationException($"Project '{projectName}' not found in current directory."); - - return fileSystem - .FileSystem - .Path - .GetRelativePath(fileSystem.AtomRootDirectory, projectPath.FullName) - .Replace("\\", "/"); - } -} diff --git a/DecSm.Atom.Module.DevopsWorkflows/Generation/Options/DevopsPool.cs b/DecSm.Atom.Module.DevopsWorkflows/Generation/Options/DevopsPool.cs deleted file mode 100644 index 02f0da11..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows/Generation/Options/DevopsPool.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Generation.Options; - -[PublicAPI] -public sealed record DevopsPool : IWorkflowOption -{ - public IReadOnlyList Demands { get; init; } = []; - - public string? Name { get; init; } - - public string? Hosted { get; init; } - - public static DevopsPool WindowsLatest { get; } = new() - { - Hosted = IJobRunsOn.WindowsLatestTag, - }; - - public static DevopsPool UbuntuLatest { get; } = new() - { - Hosted = IJobRunsOn.UbuntuLatestTag, - }; - - public static DevopsPool MacOsLatest { get; } = new() - { - Hosted = IJobRunsOn.MacOsLatestTag, - }; - - public static DevopsPool SetByMatrix { get; } = new() - { - Hosted = "$(job-runs-on)", - }; -} diff --git a/DecSm.Atom.Module.DevopsWorkflows/Generation/Options/DevopsVariableGroup.cs b/DecSm.Atom.Module.DevopsWorkflows/Generation/Options/DevopsVariableGroup.cs deleted file mode 100644 index f1f72790..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows/Generation/Options/DevopsVariableGroup.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Generation.Options; - -[PublicAPI] -public record DevopsVariableGroup(string Name) : IWorkflowOption -{ - public bool AllowMultiple => true; -} diff --git a/DecSm.Atom.Module.DevopsWorkflows/_usings.cs b/DecSm.Atom.Module.DevopsWorkflows/_usings.cs deleted file mode 100644 index 51c9fdb3..00000000 --- a/DecSm.Atom.Module.DevopsWorkflows/_usings.cs +++ /dev/null @@ -1,25 +0,0 @@ -global using System.Text; -global using System.Text.RegularExpressions; -global using DecSm.Atom.Artifacts; -global using DecSm.Atom.Build.Definition; -global using DecSm.Atom.Build.Model; -global using DecSm.Atom.Hosting; -global using DecSm.Atom.Module.DevopsWorkflows.Generation; -global using DecSm.Atom.Module.DevopsWorkflows.Generation.Options; -global using DecSm.Atom.Nuget; -global using DecSm.Atom.Params; -global using DecSm.Atom.Paths; -global using DecSm.Atom.Reports; -global using DecSm.Atom.Util; -global using DecSm.Atom.Variables; -global using DecSm.Atom.Workflows.Definition; -global using DecSm.Atom.Workflows.Definition.Options; -global using DecSm.Atom.Workflows.Definition.Triggers; -global using DecSm.Atom.Workflows.Model; -global using DecSm.Atom.Workflows.Options; -global using DecSm.Atom.Workflows.Writer; -global using JetBrains.Annotations; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.DependencyInjection.Extensions; -global using Microsoft.Extensions.Hosting; -global using Microsoft.Extensions.Logging; diff --git a/DecSm.Atom.Module.Dotnet/DecSm.Atom.Module.Dotnet.props b/DecSm.Atom.Module.Dotnet/DecSm.Atom.Module.Dotnet.props deleted file mode 100644 index 48f32066..00000000 --- a/DecSm.Atom.Module.Dotnet/DecSm.Atom.Module.Dotnet.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/DecSm.Atom.Module.GitVersion/DecSm.Atom.Module.GitVersion.props b/DecSm.Atom.Module.GitVersion/DecSm.Atom.Module.GitVersion.props deleted file mode 100644 index 844bd46c..00000000 --- a/DecSm.Atom.Module.GitVersion/DecSm.Atom.Module.GitVersion.props +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/DecSm.Atom.Module.GitVersion/IGitVersion.cs b/DecSm.Atom.Module.GitVersion/IGitVersion.cs deleted file mode 100644 index c998df7a..00000000 --- a/DecSm.Atom.Module.GitVersion/IGitVersion.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace DecSm.Atom.Module.GitVersion; - -/// -/// Provides integration with GitVersion to automatically determine build ID and version information. -/// -/// -/// Implementing this interface in your build definition will configure DecSm.Atom to use -/// GitVersion for generating build IDs and version numbers, ensuring consistent and -/// semantically versioned builds based on your Git history. -/// -[PublicAPI] -[ConfigureHostBuilder] -public partial interface IGitVersion -{ - /// - /// Configures the host builder to use GitVersion for providing build ID and version information. - /// - /// The host application builder. - /// - /// This method registers as the singleton - /// implementation for and - /// as the singleton implementation for . - /// - protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) => - builder - .Services - .AddSingleton() - .AddSingleton(); -} diff --git a/DecSm.Atom.Module.GitVersion/UseGitVersionForBuildId.cs b/DecSm.Atom.Module.GitVersion/UseGitVersionForBuildId.cs deleted file mode 100644 index a858a8f4..00000000 --- a/DecSm.Atom.Module.GitVersion/UseGitVersionForBuildId.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace DecSm.Atom.Module.GitVersion; - -/// -/// Represents a workflow option to enable or disable the use of GitVersion for determining the build ID. -/// -/// -/// When this option is enabled, the build ID will be derived from GitVersion's output, -/// typically reflecting the semantic versioning of the repository. -/// -[PublicAPI] -public sealed record UseGitVersionForBuildId : ToggleWorkflowOption; diff --git a/DecSm.Atom.Module.GitVersion/_usings.cs b/DecSm.Atom.Module.GitVersion/_usings.cs deleted file mode 100644 index 62d552f9..00000000 --- a/DecSm.Atom.Module.GitVersion/_usings.cs +++ /dev/null @@ -1,14 +0,0 @@ -global using System.Diagnostics.CodeAnalysis; -global using System.Text.Json; -global using System.Text.Json.Serialization; -global using DecSm.Atom.Build.Definition; -global using DecSm.Atom.BuildInfo; -global using DecSm.Atom.Hosting; -global using DecSm.Atom.Module.Dotnet.Helpers; -global using DecSm.Atom.Paths; -global using DecSm.Atom.Process; -global using DecSm.Atom.Workflows.Definition.Options; -global using JetBrains.Annotations; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Hosting; -global using Microsoft.Extensions.Logging; diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/ExpressionsTests.cs b/DecSm.Atom.Module.GithubWorkflows.Tests/ExpressionsTests.cs deleted file mode 100644 index 0944fe47..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/ExpressionsTests.cs +++ /dev/null @@ -1,466 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests; - -[TestFixture] -public class ExpressionsTests -{ - [Test] - public void ImplicitConversion_String_To_IGithubExpression() - { - // Arrange - const string value = "test-value"; - - // Act - IGithubExpression expression = value; - - // Assert - expression.ShouldBeOfType(); - - expression - .ToString() - .ShouldBe(value); - } - - [Test] - public void ImplicitConversion_IGithubExpression_To_String() - { - // Arrange - const string value = "test-value"; - const string expectedValue = "'test-value'"; - IGithubExpression expression = new StringExpression(value); - - // Act - string result = expression; - - // Assert - result.ShouldBe(expectedValue); - } - - [Test] - public void ToString_Returns_Write_Result() - { - // Arrange - const string value = "test-value"; - const string expectedValue = "'test-value'"; - IGithubExpression expression = new StringExpression(value); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe(expectedValue); - } - - [Test] - public void LiteralExpressionExpression_Writes_ExpectedValue() - { - // Arrange - const string value = "test-value"; - var expression = new LiteralExpression(value); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe(value); - } - - [TestCase(true)] - [TestCase(false)] - public void BoolExpression_Writes_ExpectedValue(bool value) - { - // Arrange - var expression = new BoolExpression(value); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe(value - .ToString() - .ToLowerInvariant()); - } - - [Test] - public void NullExpression_Writes_Null() - { - // Arrange - var expression = new NullExpression(); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("null"); - } - - [TestCase(1)] - [TestCase(123)] - [TestCase(123.456)] - [TestCase(0.123)] - [TestCase(-123.456)] - public void NumberExpression_Writes_ExpectedValue(double value) - { - // Arrange - var expression = new NumberExpression(value); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe(value.ToString(CultureInfo.InvariantCulture)); - } - - [TestCase("test")] - [TestCase("test-value")] - [TestCase("123")] - [TestCase("test'value")] - public void StringExpression_Writes_ExpectedValue(string value) - { - // Arrange - var expression = new StringExpression(value); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe($"'{value.Replace("'", "''")}'"); - } - - [Test] - public void LogicalGroupingExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new LogicalGroupingExpression("value1"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("(value1)"); - } - - [Test] - public void IndexExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new IndexedExpression("value1"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("[value1]"); - } - - [Test] - public void PropertyExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new PropertyExpression("value1", "property1", "property2"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("value1.property1.property2"); - } - - [Test] - public void NotExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new NotExpression("value1"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("!value1"); - } - - [Test] - public void LessThanExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new LessThanExpression("value1", "value2"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("value1 < value2"); - } - - [Test] - public void LessThanOrEqualExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new LessThanOrEqualExpression("value1", "value2"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("value1 <= value2"); - } - - [Test] - public void GreaterThanExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new GreaterThanExpression("value1", "value2"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("value1 > value2"); - } - - [Test] - public void GreaterThanOrEqualExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new GreaterThanOrEqualExpression("value1", "value2"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("value1 >= value2"); - } - - [Test] - public void EqualExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new EqualExpression("value1", "value2"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("value1 == value2"); - } - - [Test] - public void NotEqualExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new NotEqualExpression("value1", "value2"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("value1 != value2"); - } - - [Test] - public void AndExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new AndExpression("value1", "value2"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("value1 && value2"); - } - - [Test] - public void OrExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new OrExpression("value1", "value2"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("value1 || value2"); - } - - [Test] - public void ContainsExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new ContainsExpression("value1", "value2"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("contains(value1, value2)"); - } - - [Test] - public void StartsWithExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new StartsWithExpression("value1", "value2"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("startsWith(value1, value2)"); - } - - [Test] - public void EndsWithExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new EndsWithExpression("value1", "value2"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("endsWith(value1, value2)"); - } - - [Test] - public void FormatExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new FormatExpression("'Hello {0} {1} {2}'", "Mona", "the", "Octocat"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("format('Hello {0} {1} {2}', Mona, the, Octocat)"); - } - - [Test] - public void JoinExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new JoinExpression("[value1, value2]", "', '"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("join([value1, value2], ', ')"); - } - - [Test] - public void ToJsonExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new ToJsonExpression("value1"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("toJSON(value1)"); - } - - [Test] - public void FromJsonExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new FromJsonExpression("value1"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("fromJSON(value1)"); - } - - [Test] - public void HashFilesExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new HashFilesExpression("**/package-lock.json"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("hashFiles(**/package-lock.json)"); - } - - [Test] - public void SuccessExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new SuccessExpression(); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("success()"); - } - - [Test] - public void AlwaysExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new AlwaysExpression(); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("always()"); - } - - [Test] - public void CancelledExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new CancelledExpression(); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("cancelled()"); - } - - [Test] - public void FailureExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new FailureExpression(); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("failure()"); - } - - [Test] - public void ConsumedVariableExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new ConsumedVariableExpression("jobName", "variableName"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("needs.jobName.outputs.variableName"); - } - - [Test] - public void ConsumedResultExpression_Writes_ExpectedValue() - { - // Arrange - var expression = new ConsumedResultExpression("jobName"); - - // Act - var result = expression.ToString(); - - // Assert - result.ShouldBe("needs.jobName.result"); - } -} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/CheckoutOptionBuild.cs b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/CheckoutOptionBuild.cs deleted file mode 100644 index c06aa979..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/CheckoutOptionBuild.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; - -[BuildDefinition] -public partial class CheckoutOptionBuild : MinimalBuildDefinition, IGithubWorkflows, ICheckoutOptionTarget -{ - public override IReadOnlyList Workflows => - [ - new("checkoutoption-workflow") - { - Triggers = [ManualTrigger.Empty], - Targets = - [ - WorkflowTargets.CheckoutOptionTarget.WithOptions(GithubCheckoutOption.Create(new("v4", - true, - "recursive", - "some-token"))), - ], - WorkflowTypes = [new GithubWorkflowType()], - }, - ]; -} - -public interface ICheckoutOptionTarget -{ - Target CheckoutOptionTarget => t => t; -} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/DependabotBuild.cs b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/DependabotBuild.cs deleted file mode 100644 index 24b48c60..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/DependabotBuild.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; - -[BuildDefinition] -public partial class DependabotBuild : MinimalBuildDefinition, IGithubWorkflows -{ - public override IReadOnlyList Workflows => - [ - Github.DependabotWorkflow(new() - { - Registries = - [ - new("registry-1", "type1", "url1") - { - Username = new LiteralExpression("username1"), - Password = new LiteralExpression("secrets.PASSWORD").Expression, - Token = new LiteralExpression("secrets.TOKEN").Expression, - }, - new("registry-2", "type2", "url2"), - ], - Updates = - [ - new("update-1", "package-ecosystem-1", "directory-1", 1, DependabotSchedule.Daily) - { - Registries = ["registry-1", "registry-2"], - Groups = - [ - new("group-1") - { - Patterns = ["pattern-1", "pattern-2"], - }, - ], - Allow = - [ - new("dependency-1") - { - Versions = ["1.0.0", "2.0.0"], - UpdateTypes = ["version-update:semver-patch", "version-update:semver-minor"], - }, - ], - Ignore = [new("dependency-2")], - VersioningStrategy = DependabotVersioningStrategy.Increase, - }, - new("update-2", "package-ecosystem-2", "directory-2", 2, DependabotSchedule.Monthly), - ], - }), - ]; -} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/EnvironmentBuild.cs b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/EnvironmentBuild.cs deleted file mode 100644 index c584bf55..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/EnvironmentBuild.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; - -[BuildDefinition] -public partial class EnvironmentBuild : MinimalBuildDefinition, IGithubWorkflows, IEnvironmentTarget -{ - public override IReadOnlyList Workflows => - [ - new("environment-workflow") - { - Triggers = [ManualTrigger.Empty], - Targets = [WorkflowTargets.EnvironmentTarget.WithOptions(DeployToEnvironment.Create("test-env-1"))], - WorkflowTypes = [new GithubWorkflowType()], - }, - ]; -} - -public interface IEnvironmentTarget -{ - Target EnvironmentTarget => t => t; -} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/GithubCustomStepBuild.cs b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/GithubCustomStepBuild.cs deleted file mode 100644 index 6d881a43..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/GithubCustomStepBuild.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; - -public sealed record GithubTestStep(string Text, GithubCustomStepOrder Order, int Priority = 0) - : GithubCustomStepOption(Order, Priority) -{ - public override void WriteStep(GithubStepWriter writer) - { - using (writer.WriteSection("- name: Test step")) - writer.WriteLine($"run: echo \"{Text}\""); - } -} - -[BuildDefinition] -public partial class GithubCustomStepBuild : MinimalBuildDefinition, IGithubWorkflows, ICustomStepTarget -{ - public override IReadOnlyList Workflows => - [ - new("github-custom-step-workflow") - { - Triggers = [ManualTrigger.Empty], - Targets = [WorkflowTargets.CustomStepTarget], - Options = - [ - new GithubTestStep("Pre step 1", GithubCustomStepOrder.BeforeTarget, 1), - new GithubTestStep("Pre step 3", GithubCustomStepOrder.BeforeTarget, 3), - new GithubTestStep("Pre step 2", GithubCustomStepOrder.BeforeTarget, 2), - new GithubTestStep("Post step 3", GithubCustomStepOrder.AfterTarget, 3), - new GithubTestStep("Post step 1", GithubCustomStepOrder.AfterTarget, 1), - new GithubTestStep("Post step 2", GithubCustomStepOrder.AfterTarget, 2), - ], - WorkflowTypes = [Github.WorkflowType], - }, - ]; -} - -public interface ICustomStepTarget -{ - Target CustomStepTarget => t => t; -} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/GithubIfBuild.cs b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/GithubIfBuild.cs deleted file mode 100644 index 63253f02..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/GithubIfBuild.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CA1822 - -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; - -[BuildDefinition] -public partial class GithubIfBuild : MinimalBuildDefinition, IGithubWorkflows -{ - private Target GithubIfTarget => t => t; - - public override IReadOnlyList Workflows => - [ - new("githubif-workflow") - { - Triggers = [ManualTrigger.Empty], - Targets = - [ - WorkflowTargets.GithubIfTarget.WithOptions( - GithubIf.Create(new GreaterThanExpression(new NumberExpression(4), new NumberExpression(3)))), - ], - WorkflowTypes = [new GithubWorkflowType()], - }, - ]; -} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/MinimalBuild.cs b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/MinimalBuild.cs deleted file mode 100644 index b7f1b247..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/MinimalBuild.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; - -[BuildDefinition] -public partial class MinimalBuild : MinimalBuildDefinition; diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/PermissionsBuild.cs b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/PermissionsBuild.cs deleted file mode 100644 index 8acf8cea..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/PermissionsBuild.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; - -[BuildDefinition] -public partial class PermissionsBuild : MinimalBuildDefinition, IGithubWorkflows, IPermissionsTarget -{ - public override IReadOnlyList Workflows => - [ - new("permissions-workflow") - { - Triggers = - [ - new GitPullRequestTrigger - { - IncludedBranches = ["main"], - }, - ], - Targets = - [ - WorkflowTargets.PermissionsTarget.WithOptions(new GithubTokenPermissionsOption - { - Actions = GithubTokenPermission.Write, - }), - ], - WorkflowTypes = [Github.WorkflowType], - Options = [GithubTokenPermissionsOption.ReadAll], - }, - ]; -} - -public interface IPermissionsTarget -{ - Target PermissionsTarget => t => t; -} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/ReleaseTriggerBuild.cs b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/ReleaseTriggerBuild.cs deleted file mode 100644 index 3c04fdf3..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/ReleaseTriggerBuild.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; - -[BuildDefinition] -public partial class ReleaseTriggerBuild : MinimalBuildDefinition, IGithubWorkflows, IReleaseTriggerTarget -{ - public override IReadOnlyList Workflows => - [ - new("releasetrigger-workflow") - { - Triggers = [GithubReleaseTrigger.OnReleased], - Targets = [WorkflowTargets.ReleaseTriggerTarget], - WorkflowTypes = [new GithubWorkflowType()], - }, - ]; -} - -public interface IReleaseTriggerTarget -{ - Target ReleaseTriggerTarget => t => t; -} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/SetupDotnetBuild.cs b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/SetupDotnetBuild.cs deleted file mode 100644 index d6580db8..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/SetupDotnetBuild.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; - -[BuildDefinition] -public partial class SetupDotnetBuild : MinimalBuildDefinition, IGithubWorkflows, ISetupDotnetTarget -{ - public override IReadOnlyList Workflows => - [ - new("setup-dotnet") - { - Triggers = [GitPushTrigger.ToMain], - Targets = [WorkflowTargets.SetupDotnetTarget.WithOptions(new SetupDotnetStep("9.0.x"))], - WorkflowTypes = [Github.WorkflowType], - }, - ]; -} - -public interface ISetupDotnetTarget -{ - Target SetupDotnetTarget => t => t; -} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/SnapshotImageBuild.cs b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/SnapshotImageBuild.cs deleted file mode 100644 index 3fc7ff0b..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/SnapshotImageBuild.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; - -[BuildDefinition] -public partial class SnapshotImageBuild : MinimalBuildDefinition, IGithubWorkflows, ISnapshotImageTarget -{ - public override IReadOnlyList Workflows => - [ - new("snapshotimageoption-workflow") - { - Triggers = [ManualTrigger.Empty], - Targets = - [ - WorkflowTargets.SnapshotImageTarget.WithOptions( - GithubSnapshotImageOption.Create(new("snapshot-image-test", "1.*.*"))), - ], - WorkflowTypes = [new GithubWorkflowType()], - }, - ]; -} - -public interface ISnapshotImageTarget -{ - Target SnapshotImageTarget => t => t; -} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.ArtifactBuild_GeneratesWorkflow.verified.txt b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.ArtifactBuild_GeneratesWorkflow.verified.txt deleted file mode 100644 index 95b1c413..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.ArtifactBuild_GeneratesWorkflow.verified.txt +++ /dev/null @@ -1,117 +0,0 @@ -name: artifact-workflow - -on: - pull_request: - branches: - - 'main' - -jobs: - - ArtifactTarget1: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: ArtifactTarget1 - id: ArtifactTarget1 - run: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget1 --skip --headless - - - name: Upload TestArtifact1 - uses: actions/upload-artifact@v4 - with: - name: TestArtifact1 - path: "${{ github.workspace }}/.github/publish/TestArtifact1" - - ArtifactTarget2: - needs: [ ArtifactTarget1 ] - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Download TestArtifact1 - uses: actions/download-artifact@v4 - with: - name: TestArtifact1 - path: "${{ github.workspace }}/.github/artifacts/TestArtifact1" - - - name: ArtifactTarget2 - id: ArtifactTarget2 - run: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget2 --skip --headless - - - name: Upload TestArtifact2 - uses: actions/upload-artifact@v4 - with: - name: TestArtifact2-Slice1 - path: "${{ github.workspace }}/.github/publish/TestArtifact2" - - - name: Upload TestArtifact2 - uses: actions/upload-artifact@v4 - with: - name: TestArtifact2-Slice2 - path: "${{ github.workspace }}/.github/publish/TestArtifact2" - - ArtifactTarget3: - needs: [ ArtifactTarget1, ArtifactTarget2 ] - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Download TestArtifact1 - uses: actions/download-artifact@v4 - with: - name: TestArtifact1 - path: "${{ github.workspace }}/.github/artifacts/TestArtifact1" - - - name: Download TestArtifact2 - uses: actions/download-artifact@v4 - with: - name: TestArtifact2-Slice1 - path: "${{ github.workspace }}/.github/artifacts/TestArtifact2" - - - name: Download TestArtifact2 - uses: actions/download-artifact@v4 - with: - name: TestArtifact2-Slice2 - path: "${{ github.workspace }}/.github/artifacts/TestArtifact2" - - - name: ArtifactTarget3 - id: ArtifactTarget3 - run: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget3 --skip --headless - - ArtifactTarget4: - needs: [ ArtifactTarget2 ] - strategy: - matrix: - slice: [ Slice1, Slice2 ] - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Download TestArtifact2 - uses: actions/download-artifact@v4 - with: - name: TestArtifact2-${{ matrix.slice }} - path: "${{ github.workspace }}/.github/artifacts/TestArtifact2" - - - name: ArtifactTarget4 - id: ArtifactTarget4 - run: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget4 --skip --headless - env: - slice: ${{ matrix.slice }} - build-slice: ${{ matrix.slice }} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.CheckoutOptionsBuild_GeneratesWorkflow.verified.txt b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.CheckoutOptionsBuild_GeneratesWorkflow.verified.txt deleted file mode 100644 index e0ccbfa0..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.CheckoutOptionsBuild_GeneratesWorkflow.verified.txt +++ /dev/null @@ -1,22 +0,0 @@ -name: checkoutoption-workflow - -on: - workflow_dispatch: - -jobs: - - CheckoutOptionTarget: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - lfs: true - submodules: recursive - token: some-token - - - name: CheckoutOptionTarget - id: CheckoutOptionTarget - run: dotnet run --project AtomTest/AtomTest.csproj CheckoutOptionTarget --skip --headless diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.DependabotBuild_GeneratesWorkflow.verified.txt b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.DependabotBuild_GeneratesWorkflow.verified.txt deleted file mode 100644 index 35697ccb..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.DependabotBuild_GeneratesWorkflow.verified.txt +++ /dev/null @@ -1,47 +0,0 @@ -version: 2 - -registries: - registry-1: - type: type1 - url: url1 - username: username1 - password: ${{ secrets.PASSWORD }} - token: ${{ secrets.TOKEN }} - registry-2: - type: type2 - url: url2 - -updates: - - package-ecosystem: "update-1" - target-branch: 'package-ecosystem-1' - directory: 'directory-1' - registries: - - registry-1 - - registry-2 - groups: - group-1: - patterns: - - 'pattern-1' - - 'pattern-2' - allow: - - dependency-name: 'dependency-1' - versions: - - '1.0.0' - - '2.0.0' - update-types: - - 'version-update:semver-patch' - - 'version-update:semver-minor' - ignore: - - dependency-name: 'dependency-2' - versioning-strategy: increase - schedule: - interval: - daily - open-pull-requests-limit: 1 - - package-ecosystem: "update-2" - target-branch: 'package-ecosystem-2' - directory: 'directory-2' - schedule: - interval: - monthly - open-pull-requests-limit: 2 diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.GithubCustomStepBuild_GeneratesWorkflow.verified.txt b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.GithubCustomStepBuild_GeneratesWorkflow.verified.txt deleted file mode 100644 index ac7771c8..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.GithubCustomStepBuild_GeneratesWorkflow.verified.txt +++ /dev/null @@ -1,37 +0,0 @@ -name: github-custom-step-workflow - -on: - workflow_dispatch: - -jobs: - - CustomStepTarget: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Test step - run: echo "Pre step 1" - - - name: Test step - run: echo "Pre step 2" - - - name: Test step - run: echo "Pre step 3" - - - name: CustomStepTarget - id: CustomStepTarget - run: dotnet run --project AtomTest/AtomTest.csproj CustomStepTarget --skip --headless - - - name: Test step - run: echo "Post step 1" - - - name: Test step - run: echo "Post step 2" - - - name: Test step - run: echo "Post step 3" diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.GithubIfBuild_GeneratesWorkflow.verified.txt b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.GithubIfBuild_GeneratesWorkflow.verified.txt deleted file mode 100644 index 2d502cbe..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.GithubIfBuild_GeneratesWorkflow.verified.txt +++ /dev/null @@ -1,20 +0,0 @@ -name: githubif-workflow - -on: - workflow_dispatch: - -jobs: - - GithubIfTarget: - runs-on: ubuntu-latest - if: 4 > 3 - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: GithubIfTarget - id: GithubIfTarget - run: dotnet run --project AtomTest/AtomTest.csproj GithubIfTarget --skip --headless diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.SetupDotnetBuild_GeneratesWorkflow.verified.txt b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.SetupDotnetBuild_GeneratesWorkflow.verified.txt deleted file mode 100644 index 87443567..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.SetupDotnetBuild_GeneratesWorkflow.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -name: setup-dotnet - -on: - push: - branches: - - 'main' - -jobs: - - SetupDotnetTarget: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - - name: SetupDotnetTarget - id: SetupDotnetTarget - run: dotnet run --project AtomTest/AtomTest.csproj SetupDotnetTarget --skip --headless diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.SnapshotImageBuild_GeneratesWorkflow.verified.txt b/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.SnapshotImageBuild_GeneratesWorkflow.verified.txt deleted file mode 100644 index 3867d92a..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.SnapshotImageBuild_GeneratesWorkflow.verified.txt +++ /dev/null @@ -1,22 +0,0 @@ -name: snapshotimageoption-workflow - -on: - workflow_dispatch: - -jobs: - - SnapshotImageTarget: - runs-on: ubuntu-latest - snapshot: - image-name: snapshot-image-test - version: 1.*.* - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: SnapshotImageTarget - id: SnapshotImageTarget - run: dotnet run --project AtomTest/AtomTest.csproj SnapshotImageTarget --skip --headless diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/_usings.cs b/DecSm.Atom.Module.GithubWorkflows.Tests/_usings.cs deleted file mode 100644 index e2580be4..00000000 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/_usings.cs +++ /dev/null @@ -1,20 +0,0 @@ -global using System.Globalization; -global using DecSm.Atom.Args; -global using DecSm.Atom.Artifacts; -global using DecSm.Atom.Build; -global using DecSm.Atom.Build.Definition; -global using DecSm.Atom.Hosting; -global using DecSm.Atom.Module.GithubWorkflows.Generation; -global using DecSm.Atom.Module.GithubWorkflows.Generation.Options; -global using DecSm.Atom.Params; -global using DecSm.Atom.TestUtils; -global using DecSm.Atom.Workflows.Definition; -global using DecSm.Atom.Workflows.Definition.Options; -global using DecSm.Atom.Workflows.Definition.Triggers; -global using DecSm.Atom.Workflows.Options; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Hosting; -global using Shouldly; -global using Spectre.Console.Testing; -global using static DecSm.Atom.TestUtils.TestUtils; -global using Target = DecSm.Atom.Build.Definition.Target; diff --git a/DecSm.Atom.Module.GithubWorkflows/DecSm.Atom.Module.GithubWorkflows.props b/DecSm.Atom.Module.GithubWorkflows/DecSm.Atom.Module.GithubWorkflows.props deleted file mode 100644 index abd62557..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/DecSm.Atom.Module.GithubWorkflows.props +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/DecSm.Atom.Module.GithubWorkflows/Expressions.cs b/DecSm.Atom.Module.GithubWorkflows/Expressions.cs deleted file mode 100644 index 4649e67b..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/Expressions.cs +++ /dev/null @@ -1,756 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows; - -/// -/// Represents an abstract base class for all GitHub Actions expressions. -/// -/// -/// This class provides a fluent API for constructing GitHub Actions expressions -/// that can be used in workflow definitions. -/// -[PublicAPI] -public abstract record IGithubExpression -{ - public string Expression => $"${{{{ {Write()} }}}}"; - - /// - /// Writes the expression to its GitHub Actions string representation. - /// - /// The GitHub Actions expression string. - protected abstract string Write(); - - /// - public override string ToString() => - Write(); - - /// - /// Implicitly converts an to its string representation. - /// - /// The expression to convert. - /// The GitHub Actions expression string. - public static implicit operator string(IGithubExpression expression) => - expression.Write(); - - /// - /// Implicitly converts a string literal to a . - /// - /// The string value. - /// A . - public static implicit operator IGithubExpression(string value) => - new LiteralExpression(value); - - /// - /// Wraps the current expression in parentheses for logical grouping. - /// - /// A . - public IGithubExpression Grouped() => - new LogicalGroupingExpression(this); - - /// - /// Wraps the current expression in single quotes, treating it as a string literal. - /// - /// A . - public IGithubExpression AsString() => - new StringExpression(this); - - /// - /// Accesses an element of an array or object by index. - /// - /// An . - public IGithubExpression Indexed() => - new IndexedExpression(this); - - /// - /// Accesses a property of an object. - /// - /// The property names or expressions representing the path to the property. - /// A . - public IGithubExpression Property(params IGithubExpression[] sections) => - new PropertyExpression([this, .. sections]); - - /// - /// Applies the logical NOT operator to the expression. - /// - /// A . - public IGithubExpression Not() => - new NotExpression(this); - - /// - /// Creates a less than comparison expression. - /// - /// The right-hand side of the comparison. - /// A . - public IGithubExpression LessThan(IGithubExpression right) => - new LessThanExpression(this, right); - - /// - /// Creates a less than or equal to comparison expression. - /// - /// The right-hand side of the comparison. - /// A . - public IGithubExpression LessThanOrEqualTo(IGithubExpression right) => - new LessThanOrEqualExpression(this, right); - - /// - /// Creates a greater than comparison expression. - /// - /// The right-hand side of the comparison. - /// A . - public IGithubExpression GreaterThan(IGithubExpression right) => - new GreaterThanExpression(this, right); - - /// - /// Creates a greater than or equal to comparison expression. - /// - /// The right-hand side of the comparison. - /// A . - public IGithubExpression GreaterThanOrEqualTo(IGithubExpression right) => - new GreaterThanOrEqualExpression(this, right); - - /// - /// Creates an equality comparison expression. - /// - /// The right-hand side of the comparison. - /// An . - public IGithubExpression EqualTo(IGithubExpression right) => - new EqualExpression(this, right); - - /// - /// Creates a not equal to comparison expression. - /// - /// The right-hand side of the comparison. - /// A . - public IGithubExpression NotEqualTo(IGithubExpression right) => - new NotEqualExpression(this, right); - - /// - /// Creates a logical AND expression. - /// - /// The right-hand side of the AND operation. - /// An . - public IGithubExpression And(IGithubExpression right) => - new AndExpression(this, right); - - /// - /// Creates a logical OR expression. - /// - /// The right-hand side of the OR operation. - /// An . - public IGithubExpression Or(IGithubExpression right) => - new OrExpression(this, right); - - /// - /// Creates a `contains()` function expression. - /// - /// The item to search for. - /// A . - public IGithubExpression Contains(IGithubExpression item) => - new ContainsExpression(this, item); - - /// - /// Creates a `startsWith()` function expression. - /// - /// The value to check if the string starts with. - /// A . - public IGithubExpression StartsWith(IGithubExpression searchValue) => - new StartsWithExpression(this, searchValue); - - /// - /// Creates an `endsWith()` function expression. - /// - /// The value to check if the string ends with. - /// An . - public IGithubExpression EndsWith(IGithubExpression searchValue) => - new EndsWithExpression(this, searchValue); - - /// - /// Creates a `format()` function expression. - /// - /// The values to insert into the format string. - /// A . - public IGithubExpression Format(params IGithubExpression[] replaceValues) => - new FormatExpression(this, replaceValues); - - /// - /// Creates a `join()` function expression. - /// - /// An optional separator to use when joining array elements. - /// A . - public IGithubExpression Join(IGithubExpression? optionalSeparator = null) => - new JoinExpression(this, optionalSeparator); - - /// - /// Creates a `toJSON()` function expression. - /// - /// A . - public IGithubExpression ToJson() => - new ToJsonExpression(this); - - /// - /// Creates a `fromJSON()` function expression. - /// - /// A . - public IGithubExpression FromJson() => - new FromJsonExpression(this); -} - -// Literals - -/// -/// Represents a literal string value in a GitHub Actions expression. -/// -/// The literal string value. -[PublicAPI] -public sealed record LiteralExpression(string Value) : IGithubExpression -{ - /// - protected override string Write() => - Value; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents a boolean literal value in a GitHub Actions expression. -/// -/// The boolean value. -[PublicAPI] -public sealed record BoolExpression(bool Value) : IGithubExpression -{ - /// - protected override string Write() => - Value - ? "true" - : "false"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents a null literal value in a GitHub Actions expression. -/// -[PublicAPI] -public sealed record NullExpression : IGithubExpression -{ - /// - protected override string Write() => - "null"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents a number literal value in a GitHub Actions expression. -/// -/// The numeric value. -[PublicAPI] -public sealed record NumberExpression(double Value) : IGithubExpression -{ - /// - protected override string Write() => - Value switch - { - double.NaN => "0", - double.PositiveInfinity => "2147483647", - double.NegativeInfinity => "-2147483648", - _ => Value.ToString(CultureInfo.InvariantCulture), - }; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents a string literal value in a GitHub Actions expression, ensuring proper escaping. -/// -/// The string value. -[PublicAPI] -public sealed record StringExpression(string Value) : IGithubExpression -{ - /// - protected override string Write() => - Value switch - { - null => "null", - _ => $"'{Value.Replace("'", "''")}'", // Escape single quotes by doubling them - }; - - /// - public override string ToString() => - Write(); -} - -// Operators - -/// -/// Represents a logical grouping of an expression using parentheses. -/// -/// The expression to group. -[PublicAPI] -public sealed record LogicalGroupingExpression(IGithubExpression Contents) : IGithubExpression -{ - /// - protected override string Write() => - $"({Contents})"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents an indexed access expression (e.g., `array[index]` or `object['key']`). -/// -/// The expression representing the index or key. -[PublicAPI] -public sealed record IndexedExpression(IGithubExpression Index) : IGithubExpression -{ - /// - protected override string Write() => - $"[{Index}]"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents a property access expression (e.g., `object.property` or `object.nested.property`). -/// -/// The expressions representing the property path segments. -[PublicAPI] -public sealed record PropertyExpression(params IGithubExpression[] Sections) : IGithubExpression -{ - /// - protected override string Write() => - Sections.Length switch - { - 0 => throw new ArgumentException("PropertyExpression must have at least one section."), - 1 => Sections[0] - .ToString(), - _ => string.Join(".", Sections.Select(section => section.ToString())), - }; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents a logical NOT operator expression. -/// -/// The expression to negate. -[PublicAPI] -public sealed record NotExpression(IGithubExpression Contents) : IGithubExpression -{ - /// - protected override string Write() => - $"!{Contents}"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents a less than (`<`) comparison expression. -/// -/// The left-hand side of the comparison. -/// The right-hand side of the comparison. -[PublicAPI] -public sealed record LessThanExpression(IGithubExpression Left, IGithubExpression Right) : IGithubExpression -{ - /// - protected override string Write() => - $"{Left} < {Right}"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents a less than or equal to (`<=`) comparison expression. -/// -/// The left-hand side of the comparison. -/// The right-hand side of the comparison. -[PublicAPI] -public sealed record LessThanOrEqualExpression(IGithubExpression Left, IGithubExpression Right) : IGithubExpression -{ - /// - protected override string Write() => - $"{Left} <= {Right}"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents a greater than (`>`) comparison expression. -/// -/// The left-hand side of the comparison. -/// The right-hand side of the comparison. -[PublicAPI] -public sealed record GreaterThanExpression(IGithubExpression Left, IGithubExpression Right) : IGithubExpression -{ - /// - protected override string Write() => - $"{Left} > {Right}"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents a greater than or equal to (`>=`) comparison expression. -/// -/// The left-hand side of the comparison. -/// The right-hand side of the comparison. -[PublicAPI] -public sealed record GreaterThanOrEqualExpression(IGithubExpression Left, IGithubExpression Right) : IGithubExpression -{ - /// - protected override string Write() => - $"{Left} >= {Right}"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents an equality (`==`) comparison expression. -/// -/// The left-hand side of the comparison. -/// The right-hand side of the comparison. -[PublicAPI] -public sealed record EqualExpression(IGithubExpression Left, IGithubExpression Right) : IGithubExpression -{ - /// - protected override string Write() => - $"{Left} == {Right}"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents a not equal to (`!=`) comparison expression. -/// -/// The left-hand side of the comparison. -/// The right-hand side of the comparison. -[PublicAPI] -public sealed record NotEqualExpression(IGithubExpression Left, IGithubExpression Right) : IGithubExpression -{ - /// - protected override string Write() => - $"{Left} != {Right}"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents a logical AND (`&&`) expression. -/// -/// The left-hand side of the AND operation. -/// The right-hand side of the AND operation. -[PublicAPI] -public sealed record AndExpression(IGithubExpression Left, IGithubExpression Right) : IGithubExpression -{ - /// - protected override string Write() => - $"{Left} && {Right}"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents a logical OR (`||`) expression. -/// -/// The left-hand side of the OR operation. -/// The right-hand side of the OR operation. -[PublicAPI] -public sealed record OrExpression(IGithubExpression Left, IGithubExpression Right) : IGithubExpression -{ - /// - protected override string Write() => - $"{Left} || {Right}"; - - /// - public override string ToString() => - Write(); -} - -// Functions - -/// -/// Represents the `contains()` function in GitHub Actions expressions. -/// -/// The collection or string to search within. -/// The item or substring to search for. -[PublicAPI] -public sealed record ContainsExpression(IGithubExpression Search, IGithubExpression Item) : IGithubExpression -{ - /// - protected override string Write() => - $"contains({Search}, {Item})"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents the `startsWith()` function in GitHub Actions expressions. -/// -/// The string to check. -/// The value to check if the string starts with. -[PublicAPI] -public sealed record StartsWithExpression(IGithubExpression SearchString, IGithubExpression SearchValue) - : IGithubExpression -{ - /// - protected override string Write() => - $"startsWith({SearchString}, {SearchValue})"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents the `endsWith()` function in GitHub Actions expressions. -/// -/// The string to check. -/// The value to check if the string ends with. -[PublicAPI] -public sealed record EndsWithExpression(IGithubExpression SearchString, IGithubExpression SearchValue) - : IGithubExpression -{ - /// - protected override string Write() => - $"endsWith({SearchString}, {SearchValue})"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents the `format()` function in GitHub Actions expressions. -/// -/// The format string. -/// The values to insert into the format string. -[PublicAPI] -public sealed record FormatExpression(IGithubExpression String, params IGithubExpression[] ReplaceValues) - : IGithubExpression -{ - /// - protected override string Write() => - ReplaceValues.Length switch - { - 0 => throw new ArgumentException("FormatExpression must have at least one replace value."), - 1 => $"format({String}, {ReplaceValues[0]})", - _ => - $"format({String}, {string.Join(", ", ReplaceValues.Select(replaceValue => replaceValue.ToString()))})", - }; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents the `join()` function in GitHub Actions expressions. -/// -/// The array to join. -/// An optional separator to use when joining array elements. -[PublicAPI] -public sealed record JoinExpression(IGithubExpression Array, IGithubExpression? OptionalSeparator = null) - : IGithubExpression -{ - /// - protected override string Write() => - OptionalSeparator is null - ? $"join({Array})" - : $"join({Array}, {OptionalSeparator})"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents the `toJSON()` function in GitHub Actions expressions. -/// -/// The value to convert to a JSON string. -[PublicAPI] -public sealed record ToJsonExpression(IGithubExpression Value) : IGithubExpression -{ - /// - protected override string Write() => - $"toJSON({Value})"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents the `fromJSON()` function in GitHub Actions expressions. -/// -/// The JSON string to convert to an object. -[PublicAPI] -public sealed record FromJsonExpression(IGithubExpression Value) : IGithubExpression -{ - /// - protected override string Write() => - $"fromJSON({Value})"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents the `hashFiles()` function in GitHub Actions expressions. -/// -/// One or more file paths or glob patterns to hash. -[PublicAPI] -public sealed record HashFilesExpression(params IGithubExpression[] Paths) : IGithubExpression -{ - /// - protected override string Write() => - Paths.Length switch - { - 0 => throw new ArgumentException("HashFilesExpression must have at least one path."), - 1 => $"hashFiles({Paths[0]})", - _ => $"hashFiles({string.Join(", ", Paths.Select(path => path.ToString()))})", - }; - - /// - public override string ToString() => - Write(); -} - -// Status Check Functions - -/// -/// Represents the `success()` status check function in GitHub Actions expressions. -/// -/// -/// Returns `true` when all previous steps have succeeded. -/// -[PublicAPI] -public sealed record SuccessExpression : IGithubExpression -{ - /// - protected override string Write() => - "success()"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents the `always()` status check function in GitHub Actions expressions. -/// -/// -/// Returns `true` even when previous steps have failed or been cancelled. -/// -[PublicAPI] -public sealed record AlwaysExpression : IGithubExpression -{ - /// - protected override string Write() => - "always()"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents the `cancelled()` status check function in GitHub Actions expressions. -/// -/// -/// Returns `true` if the workflow was cancelled. -/// -[PublicAPI] -public sealed record CancelledExpression : IGithubExpression -{ - /// - protected override string Write() => - "cancelled()"; - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents the `failure()` status check function in GitHub Actions expressions. -/// -/// -/// Returns `true` when any previous step has failed. -/// -[PublicAPI] -public sealed record FailureExpression : IGithubExpression -{ - /// - protected override string Write() => - "failure()"; - - /// - public override string ToString() => - Write(); -} - -// Consumed Expressions -/// -/// Represents an expression to consume a variable from a previous job's output. -/// -/// The name of the job that produced the output. -/// The name of the output variable. -[PublicAPI] -public sealed record ConsumedVariableExpression(IGithubExpression JobName, IGithubExpression VariableName) - : IGithubExpression -{ - /// - protected override string Write() => - new PropertyExpression("needs", JobName, "outputs", VariableName); - - /// - public override string ToString() => - Write(); -} - -/// -/// Represents an expression to consume the result of a previous job. -/// -/// The name of the job whose result is to be consumed. -[PublicAPI] -public sealed record ConsumedResultExpression(IGithubExpression JobName) : IGithubExpression -{ - /// - protected override string Write() => - new PropertyExpression("needs", JobName, "result"); - - /// - public override string ToString() => - Write(); -} diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/DependabotWorkflowWriter.cs b/DecSm.Atom.Module.GithubWorkflows/Generation/DependabotWorkflowWriter.cs deleted file mode 100644 index 8c27749c..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/DependabotWorkflowWriter.cs +++ /dev/null @@ -1,166 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Generation; - -internal sealed class DependabotWorkflowWriter(IAtomFileSystem fileSystem, ILogger logger) - : WorkflowFileWriter(fileSystem, logger) -{ - private readonly IAtomFileSystem _fileSystem = fileSystem; - - protected override string FileExtension => "yml"; - - protected override int TabSize => 2; - - protected override RootedPath FileLocation => _fileSystem.AtomRootDirectory / ".github"; - - protected override void WriteWorkflow(WorkflowModel workflow) - { - var dependabot = workflow - .Options - .OfType() - .Single(); - - WriteLine("version: 2"); - - if (dependabot.Registries.Count > 0) - { - WriteLine(); - - using (WriteSection("registries:")) - { - foreach (var registry in dependabot.Registries) - using (WriteSection($"{registry.Name}:")) - { - WriteLine($"type: {registry.Type}"); - WriteLine($"url: {registry.Url}"); - - if (registry.Username is not null) - WriteLine($"username: {registry.Username}"); - - if (registry.Password is not null) - WriteLine($"password: {registry.Password}"); - - if (registry.Token is not null) - WriteLine($"token: {registry.Token}"); - } - } - } - - if (dependabot.Updates.Count > 0) - { - WriteLine(); - - using (WriteSection("updates:")) - { - foreach (var update in dependabot.Updates) - using (WriteSection($"- package-ecosystem: \"{update.Ecosystem}\"")) - { - WriteLine($"target-branch: '{update.TargetBranch}'"); - WriteLine($"directory: '{update.Directory}'"); - - if (update.ExcludePaths is { Count: > 0 }) - using (WriteSection("exclude-paths:")) - { - foreach (var excludePath in update.ExcludePaths) - WriteLine($"- '{excludePath}'"); - } - - if (update.Registries.Count > 0) - using (WriteSection("registries:")) - { - foreach (var registry in update.Registries) - WriteLine($"- {registry}"); - } - - if (update.Groups.Count > 0) - using (WriteSection("groups:")) - { - foreach (var group in update.Groups) - using (WriteSection($"{group.Name}:")) - { - if (group.Patterns is not { Count: > 0 }) - continue; - - using (WriteSection("patterns:")) - { - foreach (var pattern in group.Patterns) - WriteLine($"- '{pattern}'"); - } - } - } - - if (update.Allow is { Count: > 0 }) - using (WriteSection("allow:")) - { - foreach (var dependency in update.Allow) - using (WriteSection($"- dependency-name: '{dependency.DependencyName}'")) - { - if (dependency.Versions is { Count: > 0 }) - using (WriteSection("versions:")) - { - foreach (var version in dependency.Versions) - WriteLine($"- '{version}'"); - } - - if (dependency.UpdateTypes.Count > 0) - using (WriteSection("update-types:")) - { - foreach (var updateType in dependency.UpdateTypes) - WriteLine($"- '{updateType}'"); - } - } - } - - if (update.Ignore is { Count: > 0 }) - using (WriteSection("ignore:")) - { - foreach (var dependency in update.Ignore) - using (WriteSection($"- dependency-name: '{dependency.DependencyName}'")) - { - if (dependency.Versions is { Count: > 0 }) - using (WriteSection("versions:")) - { - foreach (var version in dependency.Versions) - WriteLine($"- '{version}'"); - } - - if (dependency.UpdateTypes.Count > 0) - using (WriteSection("update-types:")) - { - foreach (var updateType in dependency.UpdateTypes) - WriteLine($"- '{updateType}'"); - } - } - } - - if (update.VersioningStrategy is not null) - WriteLine($"versioning-strategy: {update.VersioningStrategy switch - { - DependabotVersioningStrategy.Auto => "auto", - DependabotVersioningStrategy.Increase => "increase", - DependabotVersioningStrategy.IncreaseIfNecessary => "increase-if-necessary", - DependabotVersioningStrategy.LockfileOnly => "lockfile-only", - DependabotVersioningStrategy.Widen => "widen", - _ => throw new ArgumentOutOfRangeException(nameof(workflow), - nameof(update.VersioningStrategy), - $"Dependabot versioning strategy '{update.VersioningStrategy}' is not supported."), - }}"); - - using (WriteSection("schedule:")) - using (WriteSection("interval:")) - { - WriteLine(update.Schedule switch - { - DependabotSchedule.Daily => "daily", - DependabotSchedule.Weekly => "weekly", - DependabotSchedule.Monthly => "monthly", - _ => throw new ArgumentOutOfRangeException(nameof(workflow), - nameof(update.Schedule), - $"Dependabot schedule '{update.Schedule}' is not supported."), - }); - } - - WriteLine($"open-pull-requests-limit: {update.OpenPullRequestsLimit}"); - } - } - } - } -} diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/GithubStepWriter.cs b/DecSm.Atom.Module.GithubWorkflows/Generation/GithubStepWriter.cs deleted file mode 100644 index f1f627e5..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/GithubStepWriter.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Generation; - -[PublicAPI] -public sealed record GithubStepWriter(StringBuilder Builder, int BaseIndentLevel) -{ - private const int TabSize = 2; - private int _indentLevel = BaseIndentLevel; - - /// - /// Writes a line of text to the output with the current indentation. - /// - /// The text to write. If null, an empty line is written. - public void WriteLine(string? value = null) - { - if (_indentLevel > 0) - Builder.Append(new string(' ', _indentLevel)); - - Builder.AppendLine(value); - } - - /// - /// Writes a section header and returns a disposable scope that manages indentation for the section's content. - /// - /// The header text for the section. - /// A disposable object that decreases the indentation level upon disposal. - /// - /// - /// using (WriteSection("- checkout: self")) - /// WriteLine("fetchDepth: 0"); - /// - /// - public IDisposable WriteSection(string header) - { - WriteLine(header); - _indentLevel += TabSize; - - return new ActionScope(() => _indentLevel -= TabSize); - } - - public void ResetIndent() => - _indentLevel = BaseIndentLevel; - - public override string ToString() => - Builder.ToString(); -} diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/GithubWorkflowWriter.cs b/DecSm.Atom.Module.GithubWorkflows/Generation/GithubWorkflowWriter.cs deleted file mode 100644 index fd6812bc..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/GithubWorkflowWriter.cs +++ /dev/null @@ -1,907 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Generation; - -internal sealed class GithubWorkflowWriter( - IAtomFileSystem fileSystem, - IBuildDefinition buildDefinition, - BuildModel buildModel, - IParamService paramService, - ILogger logger -) : WorkflowFileWriter(fileSystem, logger) -{ - private readonly IAtomFileSystem _fileSystem = fileSystem; - - protected override string FileExtension => "yml"; - - protected override int TabSize => 2; - - protected override RootedPath FileLocation => _fileSystem.AtomRootDirectory / ".github" / "workflows"; - - protected override void WriteWorkflow(WorkflowModel workflow) - { - WriteLine($"name: {workflow.Name}"); - WriteLine(); - - WritePermissions(workflow.Options); - - using (WriteSection("on:")) - { - var manualTrigger = workflow - .Triggers - .OfType() - .FirstOrDefault(); - - if (manualTrigger is not null) - using (WriteSection("workflow_dispatch:")) - { - if (manualTrigger.Inputs?.Count > 0) - using (WriteSection("inputs:")) - { - foreach (var input in manualTrigger.Inputs) - using (WriteSection($"{input.Name}:")) - { - WriteLine($"description: {input.Description}"); - - var inputParamName = buildDefinition.ParamDefinitions - .FirstOrDefault(x => x.Value.ArgName == input.Name) - .Key; - - if (inputParamName is null) - throw new InvalidOperationException( - $"Workflow {workflow.Name} has a manual trigger input named {input.Name} that does not correspond to any parameter in the build definition"); - - switch (input) - { - case ManualBoolInput boolInput: - { - bool? defaultBoolValue = null; - - if (boolInput.DefaultValue.HasValue) - { - defaultBoolValue = boolInput.DefaultValue.Value; - } - else - { - using var defaultValuesOnlyScope = - paramService.CreateDefaultValuesOnlyScope(); - - var accessedParam = buildDefinition.AccessParam(inputParamName); - - switch (accessedParam) - { - case bool boolParam: - { - defaultBoolValue = boolParam; - - break; - } - - case string stringParam: - { - if (bool.TryParse(stringParam, out var parsedBool)) - defaultBoolValue = parsedBool; - - break; - } - } - } - - var isBoolRequired = input.Required ?? defaultBoolValue is null - ? "true" - : "false"; - - WriteLine($"required: {isBoolRequired}"); - - WriteLine("type: boolean"); - - if (defaultBoolValue.HasValue) - WriteLine($"default: {(defaultBoolValue.Value ? "true" : "false")}"); - - break; - } - - case ManualStringInput stringInput: - { - using var defaultValuesOnlyScope = - paramService.CreateDefaultValuesOnlyScope(); - - var defaultStringValue = stringInput.DefaultValue is { Length: > 0 } - ? stringInput.DefaultValue - : buildDefinition - .AccessParam(inputParamName) - ?.ToString(); - - var isStringRequired = - input.Required ?? defaultStringValue is not { Length: > 0 } - ? "true" - : "false"; - - WriteLine($"required: {isStringRequired}"); - - WriteLine("type: string"); - - if (defaultStringValue is not null) - WriteLine($"default: {defaultStringValue}"); - - break; - } - - case ManualChoiceInput choiceInput: - { - using var defaultValuesOnlyScope = - paramService.CreateDefaultValuesOnlyScope(); - - var defaultChoiceValue = choiceInput.DefaultValue is { Length: > 0 } - ? choiceInput.DefaultValue - : buildDefinition - .AccessParam(inputParamName) - ?.ToString(); - - var isChoiceRequired = - input.Required ?? defaultChoiceValue is not { Length: > 0 } - ? "true" - : "false"; - - WriteLine($"required: {isChoiceRequired}"); - - WriteLine("type: choice"); - - using (WriteSection("options:")) - { - foreach (var choice in choiceInput.Choices) - WriteLine($"- {choice}"); - } - - if (defaultChoiceValue is not null) - WriteLine($"default: {defaultChoiceValue}"); - - break; - } - } - } - } - } - - var releaseTriggers = workflow - .Triggers - .OfType() - .ToList(); - - if (releaseTriggers.Count > 0) - { - using (WriteSection("release:")) - WriteLine($"types: [ {string.Join(", ", releaseTriggers.SelectMany(x => x.Types).Distinct())} ]"); - - WriteLine(); - } - - foreach (var pullRequestTrigger in workflow.Triggers.OfType()) - using (WriteSection("pull_request:")) - { - if (pullRequestTrigger.IncludedBranches.Count > 0) - using (WriteSection("branches:")) - { - foreach (var branch in pullRequestTrigger.IncludedBranches) - WriteLine($"- '{branch}'"); - } - - if (pullRequestTrigger.ExcludedBranches.Count > 0) - using (WriteSection("branches-ignore:")) - { - foreach (var branch in pullRequestTrigger.ExcludedBranches) - WriteLine($"- '{branch}'"); - } - - if (pullRequestTrigger.IncludedPaths.Count > 0) - using (WriteSection("paths:")) - { - foreach (var path in pullRequestTrigger.IncludedPaths) - WriteLine($"- '{path}'"); - } - - if (pullRequestTrigger.ExcludedPaths.Count > 0) - using (WriteSection("paths-ignore:")) - { - foreach (var path in pullRequestTrigger.ExcludedPaths) - WriteLine($"- '{path}'"); - } - - // ReSharper disable once InvertIf - if (pullRequestTrigger.Types.Count > 0) - using (WriteSection("types:")) - { - foreach (var type in pullRequestTrigger.Types) - WriteLine($"- '{type}'"); - } - } - - foreach (var pushTrigger in workflow.Triggers.OfType()) - using (WriteSection("push:")) - { - if (pushTrigger.IncludedBranches.Count > 0) - using (WriteSection("branches:")) - { - foreach (var branch in pushTrigger.IncludedBranches) - WriteLine($"- '{branch}'"); - } - - if (pushTrigger.ExcludedBranches.Count > 0) - using (WriteSection("branches-ignore:")) - { - foreach (var branch in pushTrigger.ExcludedBranches) - WriteLine($"- '{branch}'"); - } - - if (pushTrigger.IncludedPaths.Count > 0) - using (WriteSection("paths:")) - { - foreach (var path in pushTrigger.IncludedPaths) - WriteLine($"- '{path}'"); - } - - if (pushTrigger.ExcludedPaths.Count > 0) - using (WriteSection("paths-ignore:")) - { - foreach (var path in pushTrigger.ExcludedPaths) - WriteLine($"- '{path}'"); - } - - if (pushTrigger.IncludedTags.Count > 0) - using (WriteSection("tags:")) - { - foreach (var tag in pushTrigger.IncludedTags) - WriteLine($"- '{tag}'"); - } - - // ReSharper disable once InvertIf - if (pushTrigger.ExcludedTags.Count > 0) - using (WriteSection("tags-ignore:")) - { - foreach (var tag in pushTrigger.ExcludedTags) - WriteLine($"- '{tag}'"); - } - } - } - - WriteLine(); - - using (WriteSection("jobs:")) - { - foreach (var job in workflow.Jobs) - { - WriteLine(); - WriteJob(workflow, job); - } - } - } - - private void WriteJob(WorkflowModel workflow, WorkflowJobModel job) - { - using (WriteSection($"{job.Name}:")) - { - var jobRequirementNames = job - .JobDependencies - .Distinct() - .ToList(); - - if (jobRequirementNames.Count > 0) - WriteLine($"needs: [ {string.Join(", ", jobRequirementNames)} ]"); - - if (job.MatrixDimensions.Count > 0) - using (WriteSection("strategy:")) - using (WriteSection("matrix:")) - { - foreach (var dimension in job.MatrixDimensions) - WriteLine( - $"{buildDefinition.ParamDefinitions[dimension.Name].ArgName}: [ {string.Join(", ", dimension.Values)} ]"); - } - - var githubPlatformOption = job - .Options - .Concat(workflow.Options) - .OfType() - .FirstOrDefault() ?? - GithubRunsOn.UbuntuLatest; - - var labelsDisplay = githubPlatformOption.Labels.Count is 1 - ? githubPlatformOption.Labels[0] - : $"[ {string.Join(", ", githubPlatformOption.Labels)} ]"; - - if (githubPlatformOption.Group is { Length: > 0 }) - using (WriteSection("runs-on:")) - { - WriteLine($"group: {githubPlatformOption.Group}"); - WriteLine($"labels: {labelsDisplay}"); - } - else - WriteLine($"runs-on: {labelsDisplay}"); - - var snapshotImageOption = job - .Options - .Concat(workflow.Options) - .OfType() - .FirstOrDefault(); - - if (snapshotImageOption?.Value is not null) - using (WriteSection("snapshot:")) - { - WriteLine($"image-name: {snapshotImageOption.Value.ImageName}"); - - if (!string.IsNullOrWhiteSpace(snapshotImageOption.Value.Version)) - WriteLine($"version: {snapshotImageOption.Value.Version}"); - } - - var environmentOptions = job - .Options - .Concat(workflow.Options) - .OfType() - .ToList(); - - foreach (var environmentOption in environmentOptions) - WriteLine($"environment: {environmentOption.Value}"); - - var githubIfOptions = job - .Options - .Concat(workflow.Options) - .OfType() - .ToList(); - - foreach (var githubIfOption in githubIfOptions) - WriteLine($"if: {githubIfOption.Value}"); - - WritePermissions(job.Options); - - var outputs = new List(); - - foreach (var step in job.Steps) - outputs.AddRange(buildModel.GetTarget(step.Name) - .ProducedVariables); - - if (outputs.Count > 0) - using (WriteSection("outputs:")) - { - foreach (var output in outputs) - WriteLine( - $"{buildDefinition.ParamDefinitions[output].ArgName}: ${{{{ steps.{job.Name}.outputs.{buildDefinition.ParamDefinitions[output].ArgName} }}}}"); - } - - using (WriteSection("steps:")) - { - foreach (var step in job.Steps) - { - WriteLine(); - WriteStep(workflow, step, job); - } - } - } - } - - private void WritePermissions(IReadOnlyList options) - { - var githubPermissionsOption = options - .OfType() - .FirstOrDefault(); - - if (githubPermissionsOption is null) - return; - - if (githubPermissionsOption == GithubTokenPermissionsOption.WriteAll) - WriteLine("permissions: write-all"); - else if (githubPermissionsOption == GithubTokenPermissionsOption.ReadAll) - WriteLine("permissions: read-all"); - else if (githubPermissionsOption == GithubTokenPermissionsOption.NoneAll) - WriteLine("permissions: { }"); - else - using (WriteSection("permissions:")) - { - foreach (var (key, value) in githubPermissionsOption.GetStrings) - WriteLine($"{key}: {value}"); - } - } - - private void WriteStep(WorkflowModel workflow, WorkflowStepModel step, WorkflowJobModel job) - { - if (workflow - .Options - .Concat(step.Options) - .OfType() - .FirstOrDefault() is { Value: not null } checkoutOption) - using (WriteSection("- name: Checkout")) - { - WriteLine($"uses: actions/checkout@{checkoutOption.Value.Version}"); - - using (WriteSection("with:")) - { - WriteLine("fetch-depth: 0"); - - if (checkoutOption.Value.Lfs) - WriteLine("lfs: true"); - - if (!string.IsNullOrWhiteSpace(checkoutOption.Value.Submodules)) - WriteLine($"submodules: {checkoutOption.Value.Submodules}"); - - if (!string.IsNullOrWhiteSpace(checkoutOption.Value.Token)) - WriteLine($"token: {checkoutOption.Value.Token}"); - } - } - else - using (WriteSection("- name: Checkout")) - { - WriteLine("uses: actions/checkout@v4"); - - using (WriteSection("with:")) - WriteLine("fetch-depth: 0"); - } - - var commandStepTarget = buildModel.GetTarget(step.Name); - - var matrixParams = job - .MatrixDimensions - .Select(dimension => buildDefinition.ParamDefinitions[dimension.Name].ArgName) - .Select(name => (Name: name, Value: $"${{{{ matrix.{name} }}}}")) - .ToArray(); - - var buildSlice = (Name: "build-slice", Value: string.Join("-", matrixParams.Select(x => x.Value))); - - if (!string.IsNullOrWhiteSpace(buildSlice.Value)) - matrixParams = matrixParams - .Append(buildSlice) - .ToArray(); - - var setupDotnetSteps = workflow - .Options - .Concat(step.Options) - .OfType() - .ToList(); - - if (setupDotnetSteps.Count > 0) - foreach (var setupDotnetStep in setupDotnetSteps) - using (WriteSection("- uses: actions/setup-dotnet@v4")) - { - if (setupDotnetStep.DotnetVersion is not { Length: > 0 }) - continue; - - using (WriteSection("with:")) - { - // TODO: Use this to correctly handle quotes throughout the rest of the writer - var versionQuote = setupDotnetStep.DotnetVersion.Contains('\'') - ? '"' - : '\''; - - WriteLine($"dotnet-version: {versionQuote}{setupDotnetStep.DotnetVersion}{versionQuote}"); - - var qualityQuote = setupDotnetStep.Quality.ToString()!.Contains('\'') - ? '"' - : '\''; - - if (setupDotnetStep.Quality is not null) - WriteLine( - $"dotnet-quality: {qualityQuote}{setupDotnetStep.Quality.ToString()!.ToLower()}{qualityQuote}"); - } - } - - var setupNugetSteps = workflow - .Options - .Concat(step.Options) - .OfType() - .ToList(); - - if (setupNugetSteps.Count > 0) - { - var feedsToAdd = setupNugetSteps - .SelectMany(x => x.FeedsToAdd) - .DistinctBy(x => x.FeedName) - .ToList(); - - var syncAtomToolVersionToLibraryVersion = setupNugetSteps.Any(x => x.SyncAtomToolVersionToLibraryVersion); - var toolVersion = ""; - - if (syncAtomToolVersionToLibraryVersion) - { - if (SemVer.TryParse(typeof(AtomHost).Assembly - .GetCustomAttribute() - ?.InformationalVersion ?? - "", - out var semVer)) - toolVersion = - SemVer.Parse( - $"{semVer.Prefix}{(semVer.IsPreRelease ? $"-{semVer.PreRelease}" : string.Empty)}"); - else - throw new InvalidOperationException( - "Failed to parse DecSm.Atom.Host assembly version as SemVer for syncing atom tool version"); - } - - // If we know the SetupDotnet step was run for dotnet 10+, - // then we can use the dotnet tool exec command instead of installing the tool to run it - if (setupDotnetSteps.Any(x => - SemVer.TryParse(x.DotnetVersion?.Replace("x", "0"), out var version) && version.Major >= 10)) - { - using (WriteSection("- name: Setup NuGet")) - { - using (WriteSection("run: |")) - { - foreach (var feedToAdd in feedsToAdd) - WriteLine(syncAtomToolVersionToLibraryVersion - ? $"dotnet tool exec decsm.atom.tool@{toolVersion} -y -- nuget-add --name \"{feedToAdd.FeedName}\" --url \"{feedToAdd.FeedUrl}\"" - : $"dotnet tool exec decsm.atom.tool -y -- nuget-add --name \"{feedToAdd.FeedName}\" --url \"{feedToAdd.FeedUrl}\""); - } - - WriteLine("shell: bash"); - - using (WriteSection("env:")) - { - foreach (var feedToAdd in feedsToAdd) - WriteLine( - $$$"""{{{AddNugetFeedsStep.GetEnvVarNameForFeed(feedToAdd.FeedName)}}}: ${{ secrets.{{{feedToAdd.SecretName}}} }}"""); - } - } - } - else - { - using (WriteSection("- name: Install atom tool")) - { - WriteLine("run: dotnet tool update --global DecSm.Atom.Tool"); - WriteLine("shell: bash"); - } - - WriteLine(); - - using (WriteSection("- name: Setup NuGet")) - { - using (WriteSection("run: |")) - { - foreach (var feedToAdd in feedsToAdd) - WriteLine( - $" atom nuget-add --name \"{feedToAdd.FeedName}\" --url \"{feedToAdd.FeedUrl}\""); - } - - WriteLine("shell: bash"); - - using (WriteSection("env:")) - { - foreach (var feedToAdd in feedsToAdd) - WriteLine( - $$$"""{{{AddNugetFeedsStep.GetEnvVarNameForFeed(feedToAdd.FeedName)}}}: ${{ secrets.{{{feedToAdd.SecretName}}} }}"""); - } - } - } - } - - if (commandStepTarget.ConsumedArtifacts.Count > 0) - { - foreach (var consumedArtifact in commandStepTarget.ConsumedArtifacts) - if (workflow - .Jobs - .SelectMany(x => x.Steps) - .Single(x => x.Name == consumedArtifact.TargetName) - .SuppressArtifactPublishing) - logger.LogWarning( - "Workflow {WorkflowName} command {CommandName} consumes artifact {ArtifactName} from target {SourceTargetName}, which has artifact publishing suppressed; this may cause the workflow to fail", - workflow.Name, - step.Name, - consumedArtifact.ArtifactName, - consumedArtifact.TargetName); - - if (UseCustomArtifactProvider.IsEnabled(workflow.Options)) - foreach (var slice in commandStepTarget.ConsumedArtifacts.GroupBy(a => a.BuildSlice)) - { - WriteLine(); - - WriteCommandStep(workflow, - new(nameof(IRetrieveArtifact.RetrieveArtifact)), - buildModel.GetTarget(nameof(IRetrieveArtifact.RetrieveArtifact)), - [ - ("atom-artifacts", string.Join(",", - slice - .AsEnumerable() - .Select(x => x.ArtifactName))), - slice.Key is { Length: > 0 } - ? (Name: "build-slice", Value: slice.Key) - : !string.IsNullOrWhiteSpace(buildSlice.Value) - ? buildSlice - : default, - ], - false); - } - else - foreach (var artifact in commandStepTarget.ConsumedArtifacts) - { - WriteLine(); - - using (WriteSection($"- name: Download {artifact.ArtifactName}")) - { - WriteLine("uses: actions/download-artifact@v4"); - - using (WriteSection("with:")) - { - WriteLine(artifact.BuildSlice is { Length: > 0 } - ? $"name: {artifact.ArtifactName}-{artifact.BuildSlice}" - : !string.IsNullOrWhiteSpace(buildSlice.Value) - ? $"name: {artifact.ArtifactName}-{buildSlice.Value}" - : $"name: {artifact.ArtifactName}"); - - WriteLine($"path: \"{Github.PipelineArtifactDirectory}/{artifact.ArtifactName}\""); - } - } - } - } - - WriteLine(); - WriteCommandStep(workflow, step, commandStepTarget, matrixParams, true); - - // ReSharper disable once InvertIf - if (commandStepTarget.ProducedArtifacts.Count > 0 && !step.SuppressArtifactPublishing) - { - if (UseCustomArtifactProvider.IsEnabled(workflow.Options)) - foreach (var slice in commandStepTarget.ProducedArtifacts.GroupBy(a => a.BuildSlice)) - { - WriteLine(); - - WriteCommandStep(workflow, - new(nameof(IStoreArtifact.StoreArtifact)), - buildModel.GetTarget(nameof(IStoreArtifact.StoreArtifact)), - [ - ("atom-artifacts", string.Join(",", - slice - .AsEnumerable() - .Select(x => x.ArtifactName))), - slice.Key is { Length: > 0 } - ? (Name: "build-slice", Value: slice.Key) - : !string.IsNullOrWhiteSpace(buildSlice.Value) - ? buildSlice - : default, - ], - false); - } - else - foreach (var artifact in commandStepTarget.ProducedArtifacts) - { - WriteLine(); - - using (WriteSection($"- name: Upload {artifact.ArtifactName}")) - { - WriteLine("uses: actions/upload-artifact@v4"); - - using (WriteSection("with:")) - { - WriteLine(artifact.BuildSlice is { Length: > 0 } - ? $"name: {artifact.ArtifactName}-{artifact.BuildSlice}" - : !string.IsNullOrWhiteSpace(buildSlice.Value) - ? $"name: {artifact.ArtifactName}-{buildSlice.Value}" - : $"name: {artifact.ArtifactName}"); - - WriteLine($"path: \"{Github.PipelinePublishDirectory}/{artifact.ArtifactName}\""); - } - } - } - } - } - - private void WriteCommandStep( - WorkflowModel workflow, - WorkflowStepModel workflowStep, - TargetModel target, - (string name, string value)[] extraParams, - bool includeId) - { - var customPreTargetSteps = workflowStep - .Options - .Concat(workflow.Options) - .OfType() - .Where(x => x.Order is GithubCustomStepOrder.BeforeTarget) - .OrderBy(x => x.Priority) - .ToList(); - - if (customPreTargetSteps.Count > 0) - { - var writer = new GithubStepWriter(StringBuilder, IndentLevel); - - foreach (var customPostStep in customPreTargetSteps) - { - customPostStep.WriteStep(writer); - writer.ResetIndent(); - WriteLine(); - } - } - - using (WriteSection($"- name: {workflowStep.Name}")) - { - if (includeId) - WriteLine($"id: {workflowStep.Name}"); - - if (_fileSystem.IsFileBasedApp) - { - if (AppContext.GetData("EntryPointFilePath") is not string fileName) - throw new InvalidOperationException("EntryPointFilePath is null"); - - var filePathRelativeToRoot = - _fileSystem.FileSystem.Path.GetRelativePath(_fileSystem.AtomRootDirectory, fileName); - - WriteLine($"run: dotnet run --file {filePathRelativeToRoot} {workflowStep.Name} --skip --headless"); - } - else - { - var projectPath = FindProjectPath(_fileSystem, _fileSystem.ProjectName); - WriteLine($"run: dotnet run --project {projectPath} {workflowStep.Name} --skip --headless"); - } - - var env = new Dictionary(); - - foreach (var githubManualTrigger in workflow.Triggers.OfType()) - { - if (githubManualTrigger.Inputs is null or []) - continue; - - foreach (var input in githubManualTrigger.Inputs.Where(i => target - .Params - .Select(p => p.Param.ArgName) - .Any(p => p == i.Name))) - env[input.Name] = $"${{{{ inputs.{input.Name} }}}}"; - } - - foreach (var consumedVariable in target.ConsumedVariables) - env[buildDefinition.ParamDefinitions[consumedVariable.VariableName].ArgName] = - $"${{{{ needs.{consumedVariable.TargetName}.outputs.{buildDefinition.ParamDefinitions[consumedVariable.VariableName].ArgName} }}}}"; - - var requiredSecrets = target - .Params - .Where(x => x.Param.IsSecret) - .Select(x => x) - .ToArray(); - - if (requiredSecrets.Any(x => x.Param.IsSecret)) - { - foreach (var injectedSecret in workflow.Options.OfType()) - { - if (injectedSecret.Value is null) - { - logger.LogWarning( - "Workflow {WorkflowName} command {CommandName} has a secret injection with a null value", - workflow.Name, - workflowStep.Name); - - continue; - } - - var paramDefinition = buildDefinition.ParamDefinitions.GetValueOrDefault(injectedSecret.Value); - - if (paramDefinition is not null) - env[paramDefinition.ArgName] = - $"${{{{ secrets.{paramDefinition.ArgName.ToUpper().Replace('-', '_')} }}}}"; - } - - foreach (var injectedEvVar in workflow.Options.OfType()) - { - if (injectedEvVar.Value is null) - { - logger.LogWarning( - "Workflow {WorkflowName} command {CommandName} has a secret provider environment variable injection with a null value", - workflow.Name, - workflowStep.Name); - - continue; - } - - var paramDefinition = buildDefinition.ParamDefinitions.GetValueOrDefault(injectedEvVar.Value); - - if (paramDefinition is not null) - env[paramDefinition.ArgName] = - $"${{{{ vars.{paramDefinition.ArgName.ToUpper().Replace('-', '_')} }}}}"; - } - } - - foreach (var requiredSecret in requiredSecrets) - { - var injectedSecret = workflow - .Options - .Concat(workflowStep.Options) - .OfType() - .FirstOrDefault(x => x.Value == requiredSecret.Param.Name); - - if (injectedSecret is not null) - env[requiredSecret.Param.ArgName] = - $"${{{{ secrets.{requiredSecret.Param.ArgName.ToUpper().Replace('-', '_')} }}}}"; - } - - var environmentInjections = workflow.Options.OfType(); - var paramInjections = workflow.Options.OfType(); - environmentInjections = environmentInjections.Where(e => paramInjections.All(p => p.Name != e.Value)); - - foreach (var environmentInjection in environmentInjections.Where(e => e.Value is not null)) - { - if (!buildDefinition.ParamDefinitions.TryGetValue(environmentInjection.Value!, out var paramDefinition)) - { - logger.LogWarning( - "Workflow {WorkflowName} command {CommandName} has an injection for parameter {ParamName} that does not exist", - workflow.Name, - workflowStep.Name, - environmentInjection.Value); - - continue; - } - - env[paramDefinition.ArgName] = $"${{{{ vars.{paramDefinition.ArgName.ToUpper().Replace('-', '_')} }}}}"; - } - - foreach (var paramInjection in paramInjections) - { - if (!buildDefinition.ParamDefinitions.TryGetValue(paramInjection.Name, out var paramDefinition)) - { - logger.LogWarning( - "Workflow {WorkflowName} command {CommandName} has an injection for parameter {ParamName} that is not consumed by the command", - workflow.Name, - workflowStep.Name, - paramInjection.Name); - - continue; - } - - env[paramDefinition.ArgName] = paramInjection.Value; - } - - var validEnv = env - .Where(static x => x.Value is { Length: > 0 }) - .ToList(); - - var validExtraParams = extraParams - .Where(static x => x.value is { Length: > 0 }) - .ToList(); - - // ReSharper disable once InvertIf - if (validEnv.Count > 0 || validExtraParams.Count > 0) - using (WriteSection("env:")) - { - foreach (var (key, value) in validEnv) - WriteLine($"{key}: {value}"); - - foreach (var (key, value) in validExtraParams) - WriteLine($"{key}: {value}"); - } - } - - var customPostTargetSteps = workflowStep - .Options - .Concat(workflow.Options) - .OfType() - .Where(x => x.Order is GithubCustomStepOrder.AfterTarget) - .OrderBy(x => x.Priority) - .ToList(); - - if (customPostTargetSteps.Count > 0) - { - var writer = new GithubStepWriter(StringBuilder, IndentLevel); - - foreach (var customPostStep in customPostTargetSteps) - { - WriteLine(); - customPostStep.WriteStep(writer); - writer.ResetIndent(); - } - } - } - - private static string FindProjectPath(IAtomFileSystem fileSystem, string projectName) - { - var projectPath = fileSystem - .FileSystem - .DirectoryInfo - .New(fileSystem.AtomRootDirectory) - .EnumerateFiles("*.csproj", - new EnumerationOptions - { - IgnoreInaccessible = true, - MaxRecursionDepth = 4, - RecurseSubdirectories = true, - ReturnSpecialDirectories = false, - }) - .FirstOrDefault(f => f.Name.Equals($"{projectName}.csproj", StringComparison.OrdinalIgnoreCase)); - - if (projectPath?.FullName is null) - throw new InvalidOperationException($"Project '{projectName}' not found in current directory."); - - return fileSystem - .FileSystem - .Path - .GetRelativePath(fileSystem.AtomRootDirectory, projectPath.FullName) - .Replace("\\", "/"); - } -} diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/DependabotOptions.cs b/DecSm.Atom.Module.GithubWorkflows/Generation/Options/DependabotOptions.cs deleted file mode 100644 index 0acf6923..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/DependabotOptions.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Generation.Options; - -[PublicAPI] -public sealed record DependabotOptions : IWorkflowOption -{ - public required IReadOnlyList Registries { get; init; } - - public required IReadOnlyList Updates { get; init; } -} - -[PublicAPI] -public sealed record DependabotRegistry(string Name, string Type, string Url) -{ - public IGithubExpression? Username { get; init; } - - public IGithubExpression? Password { get; init; } - - public IGithubExpression? Token { get; init; } -} - -[PublicAPI] -public sealed record DependabotUpdate( - string Ecosystem, - string TargetBranch = "main", - string Directory = "/", - int OpenPullRequestsLimit = 10, - DependabotSchedule Schedule = DependabotSchedule.Weekly, - DependabotVersioningStrategy? VersioningStrategy = null -) -{ - public IReadOnlyCollection Registries { get; init; } = []; - - public IReadOnlyCollection Groups { get; init; } = []; - - public IReadOnlyCollection ExcludePaths { get; init; } = []; - - public IReadOnlyCollection Allow { get; init; } = []; - - public IReadOnlyCollection Ignore { get; init; } = []; -} - -[PublicAPI] -public sealed record DependabotUpdateGroup(string Name) -{ - public required IReadOnlyList? Patterns { get; init; } -} - -[PublicAPI] -public enum DependabotSchedule -{ - Daily, - Weekly, - Monthly, -} - -[PublicAPI] -public enum DependabotVersioningStrategy -{ - Auto, - Increase, - IncreaseIfNecessary, - LockfileOnly, - Widen, -} - -[PublicAPI] -public sealed record DependabotDependency(string DependencyName) -{ - public IReadOnlyCollection Versions { get; init; } = []; - - public IReadOnlyCollection UpdateTypes { get; init; } = []; -} - -[PublicAPI] -public static class DependabotValues -{ - public static string NugetType => "nuget-feed"; - - public static string NugetEcosystem => "nuget"; - - public static string NugetUrl => "https://api.nuget.org/v3/index.json"; -} diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubCustomStepOption.cs b/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubCustomStepOption.cs deleted file mode 100644 index f5bb42cc..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubCustomStepOption.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Generation.Options; - -[PublicAPI] -public abstract record GithubCustomStepOption(GithubCustomStepOrder Order, int Priority = 0) : IGithubCustomStepOption -{ - public abstract void WriteStep(GithubStepWriter writer); -} diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubCustomStepOrder.cs b/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubCustomStepOrder.cs deleted file mode 100644 index 41c5e36e..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubCustomStepOrder.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Generation.Options; - -[PublicAPI] -public enum GithubCustomStepOrder -{ - BeforeTarget, - AfterTarget, -} diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubIf.cs b/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubIf.cs deleted file mode 100644 index 86a527f9..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubIf.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Generation.Options; - -public record GithubIf : WorkflowOption; diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubReleaseTrigger.cs b/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubReleaseTrigger.cs deleted file mode 100644 index efbf8eaf..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubReleaseTrigger.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Generation.Options; - -[PublicAPI] -public sealed record GithubReleaseTrigger : IWorkflowTrigger -{ - public IReadOnlyList Types { get; init; } = []; - - public static GithubReleaseTrigger OnReleased { get; } = new() - { - Types = ["released"], - }; - - public static GithubReleaseTrigger OnPublished { get; } = new() - { - Types = ["published"], - }; -} diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubRunsOn.cs b/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubRunsOn.cs deleted file mode 100644 index 8ec53d7d..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubRunsOn.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Generation.Options; - -[PublicAPI] -public sealed record GithubRunsOn : IWorkflowOption -{ - public IReadOnlyList Labels { get; init; } = []; - - public string? Group { get; init; } - - public static GithubRunsOn WindowsLatest { get; } = new() - { - Labels = [IJobRunsOn.WindowsLatestTag], - }; - - public static GithubRunsOn UbuntuLatest { get; } = new() - { - Labels = [IJobRunsOn.UbuntuLatestTag], - }; - - public static GithubRunsOn MacOsLatest { get; } = new() - { - Labels = [IJobRunsOn.MacOsLatestTag], - }; - - public static GithubRunsOn SetByMatrix { get; } = new() - { - Labels = ["${{ matrix.job-runs-on }}"], - }; -} diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubTokenPermissionsOption.cs b/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubTokenPermissionsOption.cs deleted file mode 100644 index 08ceae25..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/GithubTokenPermissionsOption.cs +++ /dev/null @@ -1,121 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Generation.Options; - -[PublicAPI] -public sealed record GithubTokenPermissionsOption : IWorkflowOption -{ - public GithubTokenPermission? Actions { get; init; } - - public GithubTokenPermission? Attestations { get; init; } - - public GithubTokenPermission? Checks { get; init; } - - public GithubTokenPermission? Contents { get; init; } - - public GithubTokenPermission? Deployments { get; init; } - - public GithubTokenPermission? IdToken { get; init; } - - public GithubTokenPermission? Issues { get; init; } - - public GithubTokenPermission? Discussions { get; init; } - - public GithubTokenPermission? Packages { get; init; } - - public GithubTokenPermission? Pages { get; init; } - - public GithubTokenPermission? PullRequests { get; init; } - - public GithubTokenPermission? SecurityEvents { get; init; } - - public GithubTokenPermission? Statuses { get; init; } - - public static GithubTokenPermissionsOption NoneAll { get; } = new() - { - Actions = GithubTokenPermission.None, - Attestations = GithubTokenPermission.None, - Checks = GithubTokenPermission.None, - Contents = GithubTokenPermission.None, - Deployments = GithubTokenPermission.None, - IdToken = GithubTokenPermission.None, - Issues = GithubTokenPermission.None, - Discussions = GithubTokenPermission.None, - Packages = GithubTokenPermission.None, - Pages = GithubTokenPermission.None, - PullRequests = GithubTokenPermission.None, - SecurityEvents = GithubTokenPermission.None, - Statuses = GithubTokenPermission.None, - }; - - public static GithubTokenPermissionsOption ReadAll { get; } = new() - { - Actions = GithubTokenPermission.Read, - Attestations = GithubTokenPermission.Read, - Checks = GithubTokenPermission.Read, - Contents = GithubTokenPermission.Read, - Deployments = GithubTokenPermission.Read, - IdToken = GithubTokenPermission.Read, - Issues = GithubTokenPermission.Read, - Discussions = GithubTokenPermission.Read, - Packages = GithubTokenPermission.Read, - Pages = GithubTokenPermission.Read, - PullRequests = GithubTokenPermission.Read, - SecurityEvents = GithubTokenPermission.Read, - Statuses = GithubTokenPermission.Read, - }; - - public static GithubTokenPermissionsOption WriteAll { get; } = new() - { - Actions = GithubTokenPermission.Write, - Attestations = GithubTokenPermission.Write, - Checks = GithubTokenPermission.Write, - Contents = GithubTokenPermission.Write, - Deployments = GithubTokenPermission.Write, - IdToken = GithubTokenPermission.Write, - Issues = GithubTokenPermission.Write, - Discussions = GithubTokenPermission.Write, - Packages = GithubTokenPermission.Write, - Pages = GithubTokenPermission.Write, - PullRequests = GithubTokenPermission.Write, - SecurityEvents = GithubTokenPermission.Write, - Statuses = GithubTokenPermission.Write, - }; - - public List<(string, string)> GetStrings => - new List<(string, string?)> - { - ("actions", GetTokenPermissionString(Actions)), - ("attestations", GetTokenPermissionString(Attestations)), - ("checks", GetTokenPermissionString(Checks)), - ("contents", GetTokenPermissionString(Contents)), - ("deployments", GetTokenPermissionString(Deployments)), - ("id-token", GetTokenPermissionString(IdToken)), - ("issues", GetTokenPermissionString(Issues)), - ("discussions", GetTokenPermissionString(Discussions)), - ("packages", GetTokenPermissionString(Packages)), - ("pages", GetTokenPermissionString(Pages)), - ("pull-requests", GetTokenPermissionString(PullRequests)), - ("security-events", GetTokenPermissionString(SecurityEvents)), - ("statuses", GetTokenPermissionString(Statuses)), - } - .Where(x => x.Item2 is not null) - .Select(x => (x.Item1, x.Item2!)) - .ToList(); - - private static string? GetTokenPermissionString(GithubTokenPermission? permission) => - permission switch - { - GithubTokenPermission.None => "none", - GithubTokenPermission.Read => "read", - GithubTokenPermission.Write => "write", - null => null, - _ => throw new ArgumentOutOfRangeException(nameof(permission), permission, null), - }; -} - -[PublicAPI] -public enum GithubTokenPermission -{ - None, - Read, - Write, -} diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/IGithubCustomStepOption.cs b/DecSm.Atom.Module.GithubWorkflows/Generation/Options/IGithubCustomStepOption.cs deleted file mode 100644 index 86c403e6..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/Options/IGithubCustomStepOption.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Generation.Options; - -[PublicAPI] -public interface IGithubCustomStepOption : IWorkflowOption -{ - GithubCustomStepOrder Order { get; } - - int Priority { get; } - - bool IWorkflowOption.AllowMultiple => true; - - void WriteStep(GithubStepWriter writer); -} diff --git a/DecSm.Atom.Module.GithubWorkflows/GithubCheckoutOption.cs b/DecSm.Atom.Module.GithubWorkflows/GithubCheckoutOption.cs deleted file mode 100644 index 1a6ce2f0..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/GithubCheckoutOption.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows; - -/// -/// Represents a workflow option for configuring the `actions/checkout` step in GitHub Actions. -/// -/// -/// This option allows customization of the checkout action, such as specifying the version, -/// enabling LFS, handling submodules, and providing a custom token. -/// -[PublicAPI] -public sealed record - GithubCheckoutOption : WorkflowOption -{ - /// - public override bool AllowMultiple => false; - - /// - /// Defines the configurable values for the `actions/checkout` step. - /// - /// The version of the `actions/checkout` action to use (e.g., "v4"). - /// Whether to enable Git LFS support. - /// How to handle submodules (e.g., "true", "recursive", "false"). - /// An optional GitHub token to use for checkout, overriding the default `GITHUB_TOKEN`. - [PublicAPI] - public sealed record GithubCheckoutOptionValues( - string Version = "v4", - bool Lfs = false, - string? Submodules = null, - string? Token = null - ); -} diff --git a/DecSm.Atom.Module.GithubWorkflows/GithubSnapshotImageOption.cs b/DecSm.Atom.Module.GithubWorkflows/GithubSnapshotImageOption.cs deleted file mode 100644 index f484500e..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/GithubSnapshotImageOption.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace DecSm.Atom.Module.GithubWorkflows; - -/// -/// Represents a workflow option for specifying a custom snapshot image for GitHub Actions runners. -/// -/// -/// This option allows users to define a specific Docker image and an optional version -/// to be used as the runner environment for a GitHub Actions job, enabling custom -/// toolchains or environments. -/// -[PublicAPI] -public sealed record GithubSnapshotImageOption : WorkflowOption -{ - /// - public override bool AllowMultiple => false; - - /// - /// Defines the values for specifying a custom snapshot image. - /// - /// The name of the Docker image to use (e.g., "ubuntu-22.04"). - /// An optional version tag for the Docker image (e.g., "latest", "20231026"). - public sealed record GithubSnapshotImageValues(string ImageName, string? Version); -} diff --git a/DecSm.Atom.Module.GithubWorkflows/_usings.cs b/DecSm.Atom.Module.GithubWorkflows/_usings.cs deleted file mode 100644 index 0c68523e..00000000 --- a/DecSm.Atom.Module.GithubWorkflows/_usings.cs +++ /dev/null @@ -1,31 +0,0 @@ -global using System.Globalization; -global using System.IO.Compression; -global using System.Reflection; -global using System.Text; -global using DecSm.Atom.Artifacts; -global using DecSm.Atom.Build; -global using DecSm.Atom.Build.Definition; -global using DecSm.Atom.Build.Model; -global using DecSm.Atom.Hosting; -global using DecSm.Atom.Logging; -global using DecSm.Atom.Module.GithubWorkflows.Generation; -global using DecSm.Atom.Module.GithubWorkflows.Generation.Options; -global using DecSm.Atom.Nuget; -global using DecSm.Atom.Params; -global using DecSm.Atom.Paths; -global using DecSm.Atom.Reports; -global using DecSm.Atom.Util.Scope; -global using DecSm.Atom.Variables; -global using DecSm.Atom.Workflows.Definition; -global using DecSm.Atom.Workflows.Definition.Options; -global using DecSm.Atom.Workflows.Definition.Triggers; -global using DecSm.Atom.Workflows.Model; -global using DecSm.Atom.Workflows.Options; -global using DecSm.Atom.Workflows.Writer; -global using JetBrains.Annotations; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.DependencyInjection.Extensions; -global using Microsoft.Extensions.Hosting; -global using Microsoft.Extensions.Logging; -global using Octokit; -global using Octokit.Internal; diff --git a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.BuildDefinition_GeneratesSource.verified.txt b/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.BuildDefinition_GeneratesSource.verified.txt deleted file mode 100644 index 37ef154a..00000000 --- a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.BuildDefinition_GeneratesSource.verified.txt +++ /dev/null @@ -1,171 +0,0 @@ -// - -#nullable enable - -global using static TestNamespace.DefaultTestDefinition; - -using System.Diagnostics.CodeAnalysis; -using System.Linq.Expressions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using DecSm.Atom.Build.Definition; -using DecSm.Atom.Params; -using DecSm.Atom.Paths; -using DecSm.Atom.Process; - -namespace TestNamespace; - -[JetBrains.Annotations.PublicAPI] -partial class DefaultTestDefinition : DecSm.Atom.Build.Definition.IBuildDefinition, DecSm.Atom.Hosting.IConfigureHost -{ - public DefaultTestDefinition(System.IServiceProvider services) : base(services) { } - - private ILogger Logger => Services.GetRequiredService().CreateLogger("TestNamespace.DefaultTestDefinition"); - - private IAtomFileSystem FileSystem => GetService(); - - private IProcessRunner ProcessRunner => GetService(); - - private T GetService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() - where T : notnull => - typeof(T).GetInterface(nameof(IBuildDefinition)) != null - ? (T)(IBuildDefinition)this - : Services.GetRequiredService(); - - private IEnumerable GetServices<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() - where T : notnull => - typeof(T).GetInterface(nameof(IBuildDefinition)) != null - ? [(T)(IBuildDefinition)this] - : Services.GetServices(); - - [return: NotNullIfNotNull(nameof(defaultValue))] - private T? GetParam<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - Expression> parameterExpression, - T? defaultValue = default, - Func? converter = null) => - Services - .GetRequiredService() - .GetParam(parameterExpression, defaultValue, converter); - - #region Targets - - private System.Collections.Generic.IReadOnlyDictionary? _targetDefinitions; - - public override System.Collections.Generic.IReadOnlyDictionary TargetDefinitions => _targetDefinitions ??= new System.Collections.Generic.Dictionary - { - { nameof(DecSm.Atom.ISetupBuildInfo.SetupBuildInfo), ((DecSm.Atom.ISetupBuildInfo)this).SetupBuildInfo }, - }; - - public static class WorkflowTargets - { - public static readonly DecSm.Atom.Workflows.Definition.WorkflowTargetDefinition SetupBuildInfo = new(nameof(DecSm.Atom.ISetupBuildInfo.SetupBuildInfo)); - } - - private static DecSm.Atom.Workflows.Definition.WorkflowTargetDefinition WorkflowTarget(string name) => - name switch - { - nameof(DecSm.Atom.ISetupBuildInfo.SetupBuildInfo) => WorkflowTargets.SetupBuildInfo, - _ => throw new System.ArgumentException($"Target with name '{name}' is not defined in the build definition.", nameof(name)) - }; - - #endregion Targets - - #region Params - - private readonly System.Collections.Generic.IReadOnlyDictionary _paramDefinitions = new System.Collections.Generic.Dictionary() - { - { - nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildName), new(nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildName)) - { - ArgName = "build-name", - Description = "The name of the build.", - Sources = (DecSm.Atom.Params.ParamSource)47, - IsSecret = false, - ChainedParams = [], - } - }, - { - nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildId), new(nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildId)) - { - ArgName = "build-id", - Description = "The unique identifier for the build run.", - Sources = (DecSm.Atom.Params.ParamSource)47, - IsSecret = false, - ChainedParams = [], - } - }, - { - nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildVersion), new(nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildVersion)) - { - ArgName = "build-version", - Description = "The semantic version of the build.", - Sources = (DecSm.Atom.Params.ParamSource)47, - IsSecret = false, - ChainedParams = [], - } - }, - { - nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildTimestamp), new(nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildTimestamp)) - { - ArgName = "build-timestamp", - Description = "The build timestamp in Unix epoch seconds.", - Sources = (DecSm.Atom.Params.ParamSource)47, - IsSecret = false, - ChainedParams = [], - } - }, - { - nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildSlice), new(nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildSlice)) - { - ArgName = "build-slice", - Description = "An identifier for a build variation, used in matrix jobs.", - Sources = (DecSm.Atom.Params.ParamSource)47, - IsSecret = false, - ChainedParams = [], - } - }, - }; - - public override System.Collections.Generic.IReadOnlyDictionary ParamDefinitions => _paramDefinitions; - - public sealed class ParamsData(DecSm.Atom.Build.Definition.IBuildDefinition buildDefinition) - { - public ParamDefinition BuildName => buildDefinition.ParamDefinitions[nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildName)]; - public ParamDefinition BuildId => buildDefinition.ParamDefinitions[nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildId)]; - public ParamDefinition BuildVersion => buildDefinition.ParamDefinitions[nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildVersion)]; - public ParamDefinition BuildTimestamp => buildDefinition.ParamDefinitions[nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildTimestamp)]; - public ParamDefinition BuildSlice => buildDefinition.ParamDefinitions[nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildSlice)]; - } - - private ParamsData? _params; - - public ParamsData Params => _params ??= new(this); - - public override object? AccessParam(string paramName) => - paramName switch - { - nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildName) => ((DecSm.Atom.BuildInfo.IBuildInfo)this).BuildName, - nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildId) => ((DecSm.Atom.BuildInfo.IBuildInfo)this).BuildId, - nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildVersion) => ((DecSm.Atom.BuildInfo.IBuildInfo)this).BuildVersion, - nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildTimestamp) => ((DecSm.Atom.BuildInfo.IBuildInfo)this).BuildTimestamp, - nameof(DecSm.Atom.BuildInfo.IBuildInfo.BuildSlice) => ((DecSm.Atom.BuildInfo.IBuildInfo)this).BuildSlice, - _ => throw new System.ArgumentException($"Param with name '{paramName}' is not defined in the build definition.", nameof(paramName)) - }; - - #endregion Params - - #region Host - - public void ConfigureBuildHost(Microsoft.Extensions.Hosting.IHost builder) { } - - public void ConfigureBuildHostBuilder(Microsoft.Extensions.Hosting.IHostApplicationBuilder builder) - { - Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(builder.Services, static p => (DecSm.Atom.ISetupBuildInfo)p.GetRequiredService()); - Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(builder.Services, static p => (DecSm.Atom.BuildInfo.IBuildInfo)p.GetRequiredService()); - Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(builder.Services, static p => (DecSm.Atom.Variables.IVariablesHelper)p.GetRequiredService()); - Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(builder.Services, static p => (DecSm.Atom.Reports.IReportsHelper)p.GetRequiredService()); - DecSm.Atom.Secrets.IDotnetUserSecrets.ConfigureBuilder(builder); - } - - #endregion Host -} diff --git a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.DefaultDefinition_GeneratesSource.verified.txt b/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.DefaultDefinition_GeneratesSource.verified.txt deleted file mode 100644 index 9f809fcd..00000000 --- a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.DefaultDefinition_GeneratesSource.verified.txt +++ /dev/null @@ -1,158 +0,0 @@ -// - -#nullable enable - -global using static TestNamespace.DefaultTestDefinition; -using System.Diagnostics.CodeAnalysis; -using System.Linq.Expressions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using DecSm.Atom.Build.Definition; -using DecSm.Atom.Params; -using DecSm.Atom.Paths; -using DecSm.Atom.Process; - -namespace TestNamespace; - -[JetBrains.Annotations.PublicAPI] -partial class DefaultTestDefinition : DecSm.Atom.Build.Definition.IBuildDefinition, DecSm.Atom.Hosting.IConfigureHost -{ - private System.Collections.Generic.IReadOnlyDictionary? _targetDefinitions; - - public DefaultTestDefinition(System.IServiceProvider services) : base(services) { } - - private ILogger Logger => Services.GetRequiredService().CreateLogger("TestNamespace.DefaultTestDefinition"); - - private IAtomFileSystem FileSystem => GetService(); - - private IProcessRunner ProcessRunner => GetService(); - - private T GetService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() - where T : notnull => - typeof(T).GetInterface(nameof(IBuildDefinition)) != null - ? (T)(IBuildDefinition)this - : Services.GetRequiredService(); - - private IEnumerable GetServices<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() - where T : notnull => - typeof(T).GetInterface(nameof(IBuildDefinition)) != null - ? [(T)(IBuildDefinition)this] - : Services.GetServices(); - - [return: NotNullIfNotNull(nameof(defaultValue))] - private T? GetParam<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(Expression> parameterExpression, T? defaultValue = default, Func? converter = null) => - Services - .GetRequiredService() - .GetParam(parameterExpression, defaultValue, converter); - - - public override System.Collections.Generic.IReadOnlyDictionary TargetDefinitions => _targetDefinitions ??= new System.Collections.Generic.Dictionary - { - { "SetupBuildInfo", ((DecSm.Atom.ISetupBuildInfo)this).SetupBuildInfo }, - { "ValidateBuild", ((DecSm.Atom.IValidateBuild)this).ValidateBuild }, - }; - - public override System.Collections.Generic.IReadOnlyDictionary ParamDefinitions { get; } = new System.Collections.Generic.Dictionary - { - { - "BuildName", new("BuildName") - { - ArgName = "build-name", - Description = "Name of the build (Solution name if provided, otherwise the root directory name)", - Sources = (DecSm.Atom.Params.ParamSource)47, - IsSecret = false, - ChainedParams = [], - } - }, - { - "BuildId", new("BuildId") - { - ArgName = "build-id", - Description = "Build/run ID", - Sources = (DecSm.Atom.Params.ParamSource)47, - IsSecret = false, - ChainedParams = [], - } - }, - { - "BuildVersion", new("BuildVersion") - { - ArgName = "build-version", - Description = "Build version", - Sources = (DecSm.Atom.Params.ParamSource)47, - IsSecret = false, - ChainedParams = [], - } - }, - { - "BuildTimestamp", new("BuildTimestamp") - { - ArgName = "build-timestamp", - Description = "Build timestamp (seconds since unix epoch)", - Sources = (DecSm.Atom.Params.ParamSource)47, - IsSecret = false, - ChainedParams = [], - } - }, - { - "BuildSlice", new("BuildSlice") - { - ArgName = "build-slice", - Description = "Unique identifier for a variation of the build, commonly used for CI/CD matrix jobs", - Sources = (DecSm.Atom.Params.ParamSource)47, - IsSecret = false, - ChainedParams = [], - } - }, - }; - - public override object? AccessParam(string paramName) => - paramName switch - { - "BuildName" => ((DecSm.Atom.BuildInfo.IBuildInfo)this).BuildName, - "BuildId" => ((DecSm.Atom.BuildInfo.IBuildInfo)this).BuildId, - "BuildVersion" => ((DecSm.Atom.BuildInfo.IBuildInfo)this).BuildVersion, - "BuildTimestamp" => ((DecSm.Atom.BuildInfo.IBuildInfo)this).BuildTimestamp, - "BuildSlice" => ((DecSm.Atom.BuildInfo.IBuildInfo)this).BuildSlice, - _ => throw new System.ArgumentException($"Param with name '{paramName}' is not defined in the build definition.", nameof(paramName)), - }; - - [JetBrains.Annotations.PublicAPI] - private static class Targets - { - public static DecSm.Atom.Workflows.Definition.WorkflowTargetDefinition SetupBuildInfo = new("SetupBuildInfo"); - public static DecSm.Atom.Workflows.Definition.WorkflowTargetDefinition ValidateBuild = new("ValidateBuild"); - } - - private static DecSm.Atom.Workflows.Definition.WorkflowTargetDefinition Target(string name) => name switch - { - "SetupBuildInfo" => Targets.SetupBuildInfo, - "ValidateBuild" => Targets.ValidateBuild, - _ => throw new System.ArgumentException($"Target with name '{name}' is not defined in the build definition.", nameof(name)), - }; - - [JetBrains.Annotations.PublicAPI] - private static class Params - { - public static string BuildName = "BuildName"; - public static string BuildId = "BuildId"; - public static string BuildVersion = "BuildVersion"; - public static string BuildTimestamp = "BuildTimestamp"; - public static string BuildSlice = "BuildSlice"; - } - - public void ConfigureBuildHostBuilder(Microsoft.Extensions.Hosting.IHostApplicationBuilder builder) - { - Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(builder.Services, static p => (DecSm.Atom.ISetupBuildInfo)p.GetRequiredService()); - Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(builder.Services, static p => (DecSm.Atom.BuildInfo.IBuildInfo)p.GetRequiredService()); - Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(builder.Services, static p => (DecSm.Atom.Variables.IVariablesHelper)p.GetRequiredService()); - Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(builder.Services, static p => (DecSm.Atom.IValidateBuild)p.GetRequiredService()); - Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(builder.Services, static p => (DecSm.Atom.Reports.IReportsHelper)p.GetRequiredService()); - DecSm.Atom.Secrets.IDotnetUserSecrets.ConfigureBuilder(builder); - } - - public void ConfigureBuildHost(Microsoft.Extensions.Hosting.IHost builder) - { - - } -} diff --git a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.DefinitionWithChainedParam_GeneratesSource.verified.txt b/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.DefinitionWithChainedParam_GeneratesSource.verified.txt deleted file mode 100644 index 4645d9d1..00000000 --- a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.DefinitionWithChainedParam_GeneratesSource.verified.txt +++ /dev/null @@ -1,120 +0,0 @@ -// - -#nullable enable - -global using static TestNamespace.ChainedParamBuild; - -using System.Diagnostics.CodeAnalysis; -using System.Linq.Expressions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using DecSm.Atom.Build.Definition; -using DecSm.Atom.Params; -using DecSm.Atom.Paths; -using DecSm.Atom.Process; - -namespace TestNamespace; - -[JetBrains.Annotations.PublicAPI] -partial class ChainedParamBuild : DecSm.Atom.Build.Definition.IBuildDefinition -{ - public ChainedParamBuild(System.IServiceProvider services) : base(services) { } - - private ILogger Logger => Services.GetRequiredService().CreateLogger("TestNamespace.ChainedParamBuild"); - - private IAtomFileSystem FileSystem => GetService(); - - private IProcessRunner ProcessRunner => GetService(); - - private T GetService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() - where T : notnull => - typeof(T).GetInterface(nameof(IBuildDefinition)) != null - ? (T)(IBuildDefinition)this - : Services.GetRequiredService(); - - private IEnumerable GetServices<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() - where T : notnull => - typeof(T).GetInterface(nameof(IBuildDefinition)) != null - ? [(T)(IBuildDefinition)this] - : Services.GetServices(); - - [return: NotNullIfNotNull(nameof(defaultValue))] - private T? GetParam<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( - Expression> parameterExpression, - T? defaultValue = default, - Func? converter = null) => - Services - .GetRequiredService() - .GetParam(parameterExpression, defaultValue, converter); - - #region Targets - - private System.Collections.Generic.IReadOnlyDictionary? _targetDefinitions; - - public override System.Collections.Generic.IReadOnlyDictionary TargetDefinitions => _targetDefinitions ??= new System.Collections.Generic.Dictionary - { - { nameof(TestNamespace.IChainedParamTarget.ChainedParamTarget), ((TestNamespace.IChainedParamTarget)this).ChainedParamTarget }, - }; - - public static class WorkflowTargets - { - public static readonly DecSm.Atom.Workflows.Definition.WorkflowTargetDefinition ChainedParamTarget = new(nameof(TestNamespace.IChainedParamTarget.ChainedParamTarget)); - } - - private static DecSm.Atom.Workflows.Definition.WorkflowTargetDefinition WorkflowTarget(string name) => - name switch - { - nameof(TestNamespace.IChainedParamTarget.ChainedParamTarget) => WorkflowTargets.ChainedParamTarget, - _ => throw new System.ArgumentException($"Target with name '{name}' is not defined in the build definition.", nameof(name)) - }; - - #endregion Targets - - #region Params - - private readonly System.Collections.Generic.IReadOnlyDictionary _paramDefinitions = new System.Collections.Generic.Dictionary() - { - { - nameof(TestNamespace.IChainedParamTarget.Param1), new(nameof(TestNamespace.IChainedParamTarget.Param1)) - { - ArgName = "param-1", - Description = "Param 1", - Sources = (DecSm.Atom.Params.ParamSource)47, - IsSecret = false, - ChainedParams = [], - } - }, - { - nameof(TestNamespace.IChainedParamTarget.Param2), new(nameof(TestNamespace.IChainedParamTarget.Param2)) - { - ArgName = "param-2", - Description = "Param 2", - Sources = (DecSm.Atom.Params.ParamSource)47, - IsSecret = false, - ChainedParams = [ "Param1", ], - } - }, - }; - - public override System.Collections.Generic.IReadOnlyDictionary ParamDefinitions => _paramDefinitions; - - public sealed class ParamsData(DecSm.Atom.Build.Definition.IBuildDefinition buildDefinition) - { - public ParamDefinition Param1 => buildDefinition.ParamDefinitions[nameof(TestNamespace.IChainedParamTarget.Param1)]; - public ParamDefinition Param2 => buildDefinition.ParamDefinitions[nameof(TestNamespace.IChainedParamTarget.Param2)]; - } - - private ParamsData? _params; - - public ParamsData Params => _params ??= new(this); - - public override object? AccessParam(string paramName) => - paramName switch - { - nameof(TestNamespace.IChainedParamTarget.Param1) => ((TestNamespace.IChainedParamTarget)this).Param1, - nameof(TestNamespace.IChainedParamTarget.Param2) => ((TestNamespace.IChainedParamTarget)this).Param2, - _ => throw new System.ArgumentException($"Param with name '{paramName}' is not defined in the build definition.", nameof(paramName)) - }; - - #endregion Params -} diff --git a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.DefinitionWithTargetSetup_GeneratesSource.verified.txt b/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.DefinitionWithTargetSetup_GeneratesSource.verified.txt deleted file mode 100644 index dafb2394..00000000 --- a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.DefinitionWithTargetSetup_GeneratesSource.verified.txt +++ /dev/null @@ -1,62 +0,0 @@ -// - -#nullable enable - -global using static TestNamespace.TestDefinitionWithSetup; -using System.Diagnostics.CodeAnalysis; -using System.Linq.Expressions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using DecSm.Atom.Build.Definition; -using DecSm.Atom.Params; -using DecSm.Atom.Paths; -using DecSm.Atom.Process; - -namespace TestNamespace; - -[JetBrains.Annotations.PublicAPI] -partial class TestDefinitionWithSetup : DecSm.Atom.Build.Definition.IBuildDefinition -{ - // Build has no defined targets - - public TestDefinitionWithSetup(System.IServiceProvider services) : base(services) { } - - private ILogger Logger => Services.GetRequiredService().CreateLogger("TestNamespace.TestDefinitionWithSetup"); - - private IAtomFileSystem FileSystem => GetService(); - - private IProcessRunner ProcessRunner => GetService(); - - private T GetService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() - where T : notnull => - typeof(T).GetInterface(nameof(IBuildDefinition)) != null - ? (T)(IBuildDefinition)this - : Services.GetRequiredService(); - - private IEnumerable GetServices<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() - where T : notnull => - typeof(T).GetInterface(nameof(IBuildDefinition)) != null - ? [(T)(IBuildDefinition)this] - : Services.GetServices(); - - [return: NotNullIfNotNull(nameof(defaultValue))] - private T? GetParam<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(Expression> parameterExpression, T? defaultValue = default, Func? converter = null) => - Services - .GetRequiredService() - .GetParam(parameterExpression, defaultValue, converter); - - - public override System.Collections.Generic.IReadOnlyDictionary TargetDefinitions { get; } = new System.Collections.Generic.Dictionary(); - - public override System.Collections.Generic.IReadOnlyDictionary ParamDefinitions { get; } = new System.Collections.Generic.Dictionary(); - - public override object? AccessParam(string paramName) => throw new System.ArgumentException($"Param with name '{paramName}' is not defined in the build definition.", nameof(paramName)); - - // Build has no defined targets - - // Build has no defined params - - // Build has no defined HostBuilder configuration - - // Build has no defined Host configuration -} diff --git a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalDefinition_GeneratesSource.verified.txt b/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalDefinition_GeneratesSource.verified.txt deleted file mode 100644 index b62c486a..00000000 --- a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalDefinition_GeneratesSource.verified.txt +++ /dev/null @@ -1,62 +0,0 @@ -// - -#nullable enable - -global using static TestNamespace.MinimalTestDefinition; -using System.Diagnostics.CodeAnalysis; -using System.Linq.Expressions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using DecSm.Atom.Build.Definition; -using DecSm.Atom.Params; -using DecSm.Atom.Paths; -using DecSm.Atom.Process; - -namespace TestNamespace; - -[JetBrains.Annotations.PublicAPI] -partial class MinimalTestDefinition : DecSm.Atom.Build.Definition.IBuildDefinition -{ - // Build has no defined targets - - public MinimalTestDefinition(System.IServiceProvider services) : base(services) { } - - private ILogger Logger => Services.GetRequiredService().CreateLogger("TestNamespace.MinimalTestDefinition"); - - private IAtomFileSystem FileSystem => GetService(); - - private IProcessRunner ProcessRunner => GetService(); - - private T GetService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() - where T : notnull => - typeof(T).GetInterface(nameof(IBuildDefinition)) != null - ? (T)(IBuildDefinition)this - : Services.GetRequiredService(); - - private IEnumerable GetServices<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() - where T : notnull => - typeof(T).GetInterface(nameof(IBuildDefinition)) != null - ? [(T)(IBuildDefinition)this] - : Services.GetServices(); - - [return: NotNullIfNotNull(nameof(defaultValue))] - private T? GetParam<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(Expression> parameterExpression, T? defaultValue = default, Func? converter = null) => - Services - .GetRequiredService() - .GetParam(parameterExpression, defaultValue, converter); - - - public override System.Collections.Generic.IReadOnlyDictionary TargetDefinitions { get; } = new System.Collections.Generic.Dictionary(); - - public override System.Collections.Generic.IReadOnlyDictionary ParamDefinitions { get; } = new System.Collections.Generic.Dictionary(); - - public override object? AccessParam(string paramName) => throw new System.ArgumentException($"Param with name '{paramName}' is not defined in the build definition.", nameof(paramName)); - - // Build has no defined targets - - // Build has no defined params - - // Build has no defined HostBuilder configuration - - // Build has no defined Host configuration -} diff --git a/DecSm.Atom.SourceGenerators.Tests/Tests/GenerateEntryPointSourceGeneratorTests.EmptyDefinition_GeneratesDefaultSource.verified.txt b/DecSm.Atom.SourceGenerators.Tests/Tests/GenerateEntryPointSourceGeneratorTests.EmptyDefinition_GeneratesDefaultSource.verified.txt deleted file mode 100644 index 0ff7e8f4..00000000 --- a/DecSm.Atom.SourceGenerators.Tests/Tests/GenerateEntryPointSourceGeneratorTests.EmptyDefinition_GeneratesDefaultSource.verified.txt +++ /dev/null @@ -1,3 +0,0 @@ -// - -DecSm.Atom.Hosting.AtomHost.Run(args); \ No newline at end of file diff --git a/DecSm.Atom.SourceGenerators/Properties/launchSettings.json b/DecSm.Atom.SourceGenerators/Properties/launchSettings.json deleted file mode 100644 index 50e8be51..00000000 --- a/DecSm.Atom.SourceGenerators/Properties/launchSettings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "profiles": { - "DebugSourceGen - _atom": { - "commandName": "DebugRoslynComponent", - "targetProject": "../_atom/_atom.csproj" - }, - "DebugSourceGen - Process": { - "commandName": "DebugRoslynComponent", - "targetProject": "../DecSm.Atom.Extensions.Process/DecSm.Atom.Extensions.Process.csproj" - } - } -} \ No newline at end of file diff --git a/DecSm.Atom.SourceGenerators/Readme.md b/DecSm.Atom.SourceGenerators/Readme.md deleted file mode 100644 index 2e244b23..00000000 --- a/DecSm.Atom.SourceGenerators/Readme.md +++ /dev/null @@ -1,29 +0,0 @@ -# Roslyn Source Generators Sample - -A set of three projects that illustrates Roslyn source generators. Enjoy this template to learn from and modify source generators for your own needs. - -## Content -### DecSm.Atom.Gen -A .NET Standard project with implementations of sample source generators. -**You must build this project to see the result (generated code) in the IDE.** - -- [SampleSourceGenerator.cs](SampleSourceGenerator.cs): A source generator that creates C# classes based on a text file (in this case, Domain Driven Design ubiquitous language registry). -- [SampleIncrementalSourceGenerator.cs](SampleIncrementalSourceGenerator.cs): A source generator that creates a custom report based on class properties. The target class should be annotated with the `Generators.ReportAttribute` attribute. - -### DecSm.Atom.Gen.Sample -A project that references source generators. Note the parameters of `ProjectReference` in [DecSm.Atom.Gen.Sample.csproj](../DecSm.Atom.Gen.Sample/DecSm.Atom.Gen.Sample.csproj), they make sure that the project is referenced as a set of source generators. - -### DecSm.Atom.Gen.Tests -Unit tests for source generators. The easiest way to develop language-related features is to start with unit tests. - -## How To? -### How to debug? -- Use the [launchSettings.json](Properties/launchSettings.json) profile. -- Debug tests. - -### How can I determine which syntax nodes I should expect? -Consider installing the Roslyn syntax tree viewer plugin [Rossynt](https://plugins.jetbrains.com/plugin/16902-rossynt/). - -### How to learn more about wiring source generators? -Watch the walkthrough video: [Let’s Build an Incremental Source Generator With Roslyn, by Stefan Pölz](https://youtu.be/azJm_Y2nbAI) -The complete set of information is available in [Source Generators Cookbook](https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md). \ No newline at end of file diff --git a/DecSm.Atom.TestUtils/TestWorkflowOption.cs b/DecSm.Atom.TestUtils/TestWorkflowOption.cs deleted file mode 100644 index 19a07510..00000000 --- a/DecSm.Atom.TestUtils/TestWorkflowOption.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace DecSm.Atom.TestUtils; - -[PublicAPI] -public sealed record TestWorkflowOption : WorkflowOption; diff --git a/DecSm.Atom.TestUtils/TestWorkflowWriter.cs b/DecSm.Atom.TestUtils/TestWorkflowWriter.cs deleted file mode 100644 index 0a8d4b08..00000000 --- a/DecSm.Atom.TestUtils/TestWorkflowWriter.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace DecSm.Atom.TestUtils; - -[PublicAPI] -public class TestWorkflowWriter : IWorkflowWriter -{ - public bool IsDirty { get; init; } - - public List GeneratedWorkflows { get; } = []; - - public Task Generate(WorkflowModel workflow, CancellationToken cancellationToken = default) - { - GeneratedWorkflows.Add(workflow); - - return Task.CompletedTask; - } - - public Task CheckForDirtyWorkflow(WorkflowModel workflow, CancellationToken cancellationToken = default) => - Task.FromResult(IsDirty); -} diff --git a/DecSm.Atom.TestUtils/_usings.cs b/DecSm.Atom.TestUtils/_usings.cs deleted file mode 100644 index ea9f77bf..00000000 --- a/DecSm.Atom.TestUtils/_usings.cs +++ /dev/null @@ -1,21 +0,0 @@ -global using System.IO.Abstractions; -global using System.IO.Abstractions.TestingHelpers; -global using System.Text; -global using System.Text.RegularExpressions; -global using DecSm.Atom.Args; -global using DecSm.Atom.Artifacts; -global using DecSm.Atom.Build.Definition; -global using DecSm.Atom.BuildInfo; -global using DecSm.Atom.Hosting; -global using DecSm.Atom.Util.Scope; -global using DecSm.Atom.Workflows.Definition; -global using DecSm.Atom.Workflows.Definition.Options; -global using DecSm.Atom.Workflows.Definition.Triggers; -global using DecSm.Atom.Workflows.Model; -global using DecSm.Atom.Workflows.Writer; -global using JetBrains.Annotations; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Hosting; -global using Microsoft.Extensions.Logging; -global using Spectre.Console; -global using Spectre.Console.Testing; diff --git a/DecSm.Atom.Tests/ApiSurfaceTests/PublicApiSurfaceTests.VerifyPublicApiSurface.verified.txt b/DecSm.Atom.Tests/ApiSurfaceTests/PublicApiSurfaceTests.VerifyPublicApiSurface.verified.txt deleted file mode 100644 index 956e11bf..00000000 --- a/DecSm.Atom.Tests/ApiSurfaceTests/PublicApiSurfaceTests.VerifyPublicApiSurface.verified.txt +++ /dev/null @@ -1,2823 +0,0 @@ -[ - { - Name: DecSm.Atom.Args.CommandArg, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Name - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Args.CommandLineArgs, - Members: [ - { - Name: $ - }, - { - Name: Args - }, - { - Name: Commands - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: HasGen - }, - { - Name: HasHeadless - }, - { - Name: HasHelp - }, - { - Name: HasInteractive - }, - { - Name: HasProject - }, - { - Name: HasSkip - }, - { - Name: HasVerbose - }, - { - Name: IsValid - }, - { - Name: Params - }, - { - Name: ProjectName - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Args.GenArg, - Members: [ - { - Name: $ - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Args.HeadlessArg, - Members: [ - { - Name: $ - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Args.HelpArg, - Members: [ - { - Name: $ - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Args.IArg - }, - { - Name: DecSm.Atom.Args.InteractiveArg, - Members: [ - { - Name: $ - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Args.ParamArg, - Members: [ - { - Name: $ - }, - { - Name: ArgName - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ParamName - }, - { - Name: ParamValue - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Args.ProjectArg, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ProjectName - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Args.SkipArg, - Members: [ - { - Name: $ - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Args.VerboseArg, - Members: [ - { - Name: $ - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Artifacts.IArtifactProvider, - Members: [ - { - Name: Cleanup - }, - { - Name: GetStoredRunIdentifiers - }, - { - Name: RequiredParams - }, - { - Name: RetrieveArtifacts - }, - { - Name: StoreArtifacts - } - ] - }, - { - Name: DecSm.Atom.Artifacts.IAtomArtifactsParam, - Members: [ - { - Name: AtomArtifacts - } - ] - }, - { - Name: DecSm.Atom.Artifacts.IRetrieveArtifact, - Members: [ - { - Name: RetrieveArtifact - } - ] - }, - { - Name: DecSm.Atom.Artifacts.IStoreArtifact, - Members: [ - { - Name: StoreArtifact - } - ] - }, - { - Name: DecSm.Atom.Artifacts.UseCustomArtifactProvider, - Members: [ - { - Name: $ - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Build.Definition.BuildDefinition - }, - { - Name: DecSm.Atom.Build.Definition.BuildDefinitionAttribute - }, - { - Name: DecSm.Atom.Build.Definition.DefinedParam, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Param - }, - { - Name: Required - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Build.Definition.GenerateInterfaceMembersAttribute - }, - { - Name: DecSm.Atom.Build.Definition.GenerateSolutionModelAttribute - }, - { - Name: DecSm.Atom.Build.Definition.IBuildDefinition, - Members: [ - { - Name: AccessParam - }, - { - Name: GlobalWorkflowOptions - }, - { - Name: ParamDefinitions - }, - { - Name: TargetDefinitions - }, - { - Name: Workflows - } - ] - }, - { - Name: DecSm.Atom.Build.Definition.MinimalBuildDefinition, - Members: [ - { - Name: AccessParam - }, - { - Name: GlobalWorkflowOptions - }, - { - Name: ParamDefinitions - }, - { - Name: Services - }, - { - Name: TargetDefinitions - }, - { - Name: Workflows - } - ] - }, - { - Name: DecSm.Atom.Build.Definition.Target, - Members: [ - { - Name: BeginInvoke - }, - { - Name: EndInvoke - }, - { - Name: Invoke - } - ] - }, - { - Name: DecSm.Atom.Build.Definition.TargetDefinition, - Members: [ - { - Name: ConsumedArtifacts - }, - { - Name: ConsumedVariables - }, - { - Name: ConsumesArtifact - }, - { - Name: ConsumesArtifact - }, - { - Name: ConsumesArtifacts - }, - { - Name: ConsumesArtifacts - }, - { - Name: ConsumesVariable - }, - { - Name: Dependencies - }, - { - Name: DependsOn - }, - { - Name: DependsOn - }, - { - Name: DependsOn - }, - { - Name: DescribedAs - }, - { - Name: Description - }, - { - Name: Executes - }, - { - Name: Executes - }, - { - Name: Executes - }, - { - Name: Extends - }, - { - Name: Hidden - }, - { - Name: IsHidden - }, - { - Name: Name - }, - { - Name: Params - }, - { - Name: ProducedArtifacts - }, - { - Name: ProducedVariables - }, - { - Name: ProducesArtifact - }, - { - Name: ProducesArtifacts - }, - { - Name: ProducesVariable - }, - { - Name: RequiresParam - }, - { - Name: Tasks - }, - { - Name: UsesParam - } - ] - }, - { - Name: DecSm.Atom.Build.Definition.TargetDefinitionExtensions, - Members: [ - { - Name: DependsOn - }, - { - Name: DependsOn - }, - { - Name: DependsOn - }, - { - Name: DependsOn - } - ] - }, - { - Name: DecSm.Atom.Build.IBuildAccessor, - Members: [ - { - Name: Services - } - ] - }, - { - Name: DecSm.Atom.Build.Model.BuildModel, - Members: [ - { - Name: $ - }, - { - Name: CurrentTarget - }, - { - Name: DeclaringAssembly - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: GetTarget - }, - { - Name: Targets - }, - { - Name: TargetStates - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Build.Model.ConsumedArtifact, - Members: [ - { - Name: $ - }, - { - Name: ArtifactName - }, - { - Name: BuildSlice - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: TargetName - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Build.Model.ConsumedVariable, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: TargetName - }, - { - Name: ToString - }, - { - Name: VariableName - } - ] - }, - { - Name: DecSm.Atom.Build.Model.ProducedArtifact, - Members: [ - { - Name: $ - }, - { - Name: ArtifactName - }, - { - Name: BuildSlice - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Build.Model.TargetModel, - Members: [ - { - Name: $ - }, - { - Name: ConsumedArtifacts - }, - { - Name: ConsumedVariables - }, - { - Name: DeclaringAssembly - }, - { - Name: Deconstruct - }, - { - Name: Dependencies - }, - { - Name: Description - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: IsHidden - }, - { - Name: Name - }, - { - Name: Params - }, - { - Name: ProducedArtifacts - }, - { - Name: ProducedVariables - }, - { - Name: Tasks - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Build.Model.TargetRunState, - Members: [ - { - Name: Failed - }, - { - Name: NotRun - }, - { - Name: PendingRun - }, - { - Name: Running - }, - { - Name: Skipped - }, - { - Name: Succeeded - }, - { - Name: Uninitialized - }, - { - Name: value__ - } - ] - }, - { - Name: DecSm.Atom.Build.Model.TargetState, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Name - }, - { - Name: RunDuration - }, - { - Name: Status - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Build.Model.UsedParam, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Param - }, - { - Name: Required - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.BuildInfo.IBuildIdProvider, - Members: [ - { - Name: BuildId - }, - { - Name: GetBuildIdGroup - } - ] - }, - { - Name: DecSm.Atom.BuildInfo.IBuildInfo, - Members: [ - { - Name: BuildId - }, - { - Name: BuildName - }, - { - Name: BuildSlice - }, - { - Name: BuildTimestamp - }, - { - Name: BuildVersion - } - ] - }, - { - Name: DecSm.Atom.BuildInfo.IBuildTimestampProvider, - Members: [ - { - Name: Timestamp - } - ] - }, - { - Name: DecSm.Atom.BuildInfo.IBuildVersionProvider, - Members: [ - { - Name: Version - } - ] - }, - { - Name: DecSm.Atom.Help.IHelpService, - Members: [ - { - Name: ShowHelp - } - ] - }, - { - Name: DecSm.Atom.Hosting.AtomHost, - Members: [ - { - Name: CreateAtomBuilder - }, - { - Name: Run - } - ] - }, - { - Name: DecSm.Atom.Hosting.ConfigureHostAttribute - }, - { - Name: DecSm.Atom.Hosting.ConfigureHostBuilderAttribute - }, - { - Name: DecSm.Atom.Hosting.GenerateEntryPointAttribute - }, - { - Name: DecSm.Atom.Hosting.HostExtensions, - Members: [ - { - Name: AddAtom - }, - { - Name: UseAtom - } - ] - }, - { - Name: DecSm.Atom.Hosting.IConfigureHost, - Members: [ - { - Name: ConfigureBuildHost - }, - { - Name: ConfigureBuildHostBuilder - } - ] - }, - { - Name: DecSm.Atom.ISetupBuildInfo, - Members: [ - { - Name: SetupBuildInfo - } - ] - }, - { - Name: DecSm.Atom.IValidateBuild, - Members: [ - { - Name: ValidateBuild - } - ] - }, - { - Name: DecSm.Atom.Logging.LogOptions, - Members: [ - { - Name: IsVerboseEnabled - } - ] - }, - { - Name: DecSm.Atom.Nuget.AddNugetFeedsStep, - Members: [ - { - Name: $ - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: FeedsToAdd - }, - { - Name: GetEnvVarNameForFeed - }, - { - Name: GetHashCode - }, - { - Name: SyncAtomToolVersionToLibraryVersion - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Nuget.NugetFeedOptions, - Members: [ - { - Name: $ - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: FeedName - }, - { - Name: FeedUrl - }, - { - Name: GetHashCode - }, - { - Name: SecretName - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Params.IParamService, - Members: [ - { - Name: CreateDefaultValuesOnlyScope - }, - { - Name: CreateNoCacheScope - }, - { - Name: CreateOverrideSourcesScope - }, - { - Name: GetParam - }, - { - Name: GetParam - }, - { - Name: GetParam - }, - { - Name: MaskMatchingSecrets - } - ] - }, - { - Name: DecSm.Atom.Params.ParamDefinition, - Members: [ - { - Name: $ - }, - { - Name: ArgName - }, - { - Name: ChainedParams - }, - { - Name: Deconstruct - }, - { - Name: Description - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: IsSecret - }, - { - Name: Name - }, - { - Name: Sources - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Params.ParamDefinitionAttribute, - Members: [ - { - Name: ArgName - }, - { - Name: ChainedParams - }, - { - Name: Description - }, - { - Name: IsSecret - }, - { - Name: Sources - } - ] - }, - { - Name: DecSm.Atom.Params.ParamModel, - Members: [ - { - Name: $ - }, - { - Name: ArgName - }, - { - Name: ChainedParams - }, - { - Name: Deconstruct - }, - { - Name: DefaultValue - }, - { - Name: Description - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: IsSecret - }, - { - Name: Name - }, - { - Name: Sources - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Params.ParamSource, - Members: [ - { - Name: All - }, - { - Name: Cache - }, - { - Name: CommandLineArgs - }, - { - Name: Configuration - }, - { - Name: EnvironmentVariables - }, - { - Name: None - }, - { - Name: Secrets - }, - { - Name: value__ - } - ] - }, - { - Name: DecSm.Atom.Params.SecretDefinitionAttribute - }, - { - Name: DecSm.Atom.Paths.AtomPaths, - Members: [ - { - Name: Artifacts - }, - { - Name: ProvidePath - }, - { - Name: ProvidePath - }, - { - Name: Publish - }, - { - Name: Root - }, - { - Name: Temp - } - ] - }, - { - Name: DecSm.Atom.Paths.FunctionPathProvider, - Members: [ - { - Name: GetPath - }, - { - Name: Priority - }, - { - Name: Resolver - } - ] - }, - { - Name: DecSm.Atom.Paths.IAtomFileSystem, - Members: [ - { - Name: AtomArtifactsDirectory - }, - { - Name: AtomPublishDirectory - }, - { - Name: AtomRootDirectory - }, - { - Name: AtomTempDirectory - }, - { - Name: CreateRootedPath - }, - { - Name: CurrentDirectory - }, - { - Name: FileSystem - }, - { - Name: GetPath - }, - { - Name: GetPath - }, - { - Name: IsFileBasedApp - }, - { - Name: ProjectName - } - ] - }, - { - Name: DecSm.Atom.Paths.IPathLocator, - Members: [ - { - Name: Path - } - ] - }, - { - Name: DecSm.Atom.Paths.IPathProvider, - Members: [ - { - Name: GetPath - }, - { - Name: Priority - } - ] - }, - { - Name: DecSm.Atom.Paths.RootedPath, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: DirectoryExists - }, - { - Name: DirectoryName - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: FileExists - }, - { - Name: FileName - }, - { - Name: FileNameWithoutExtension - }, - { - Name: FileSystem - }, - { - Name: GetHashCode - }, - { - Name: Parent - }, - { - Name: Path - }, - { - Name: PathExists - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Paths.TransformFileScope, - Members: [ - { - Name: Add - }, - { - Name: AddAsync - }, - { - Name: CancelRestore - }, - { - Name: Create - }, - { - Name: CreateAsync - }, - { - Name: Dispose - }, - { - Name: DisposeAsync - } - ] - }, - { - Name: DecSm.Atom.Paths.TransformFileScopeExtensions, - Members: [ - { - Name: AddAsync - }, - { - Name: AddAsync - } - ] - }, - { - Name: DecSm.Atom.Paths.TransformMultiFileScope, - Members: [ - { - Name: Add - }, - { - Name: AddAsync - }, - { - Name: CancelRestore - }, - { - Name: Create - }, - { - Name: CreateAsync - }, - { - Name: Dispose - }, - { - Name: DisposeAsync - } - ] - }, - { - Name: DecSm.Atom.Process.IProcessRunner, - Members: [ - { - Name: Run - }, - { - Name: RunAsync - } - ] - }, - { - Name: DecSm.Atom.Process.ProcessRunner, - Members: [ - { - Name: Run - }, - { - Name: RunAsync - } - ] - }, - { - Name: DecSm.Atom.Process.ProcessRunOptions, - Members: [ - { - Name: $ - }, - { - Name: AllowFailedResult - }, - { - Name: Args - }, - { - Name: Deconstruct - }, - { - Name: EnvironmentVariables - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: ErrorLogLevel - }, - { - Name: GetHashCode - }, - { - Name: InvocationLogLevel - }, - { - Name: Name - }, - { - Name: OutputLogLevel - }, - { - Name: ToString - }, - { - Name: TransformError - }, - { - Name: TransformOutput - }, - { - Name: WorkingDirectory - } - ] - }, - { - Name: DecSm.Atom.Process.ProcessRunResult, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Error - }, - { - Name: ExitCode - }, - { - Name: GetHashCode - }, - { - Name: Output - }, - { - Name: RunOptions - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Reports.ArtifactReportData, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Name - }, - { - Name: Path - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Reports.ColumnAlignment, - Members: [ - { - Name: Center - }, - { - Name: Left - }, - { - Name: Right - }, - { - Name: value__ - } - ] - }, - { - Name: DecSm.Atom.Reports.ICustomReportData, - Members: [ - { - Name: BeforeStandardData - } - ] - }, - { - Name: DecSm.Atom.Reports.IOutcomeReportWriter, - Members: [ - { - Name: ReportRunOutcome - } - ] - }, - { - Name: DecSm.Atom.Reports.IReportData - }, - { - Name: DecSm.Atom.Reports.IReportsHelper, - Members: [ - { - Name: AddReportData - } - ] - }, - { - Name: DecSm.Atom.Reports.ListReportData, - Members: [ - { - Name: $ - }, - { - Name: BeforeStandardData - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Items - }, - { - Name: Prefix - }, - { - Name: Title - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Reports.LogReportData, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Exception - }, - { - Name: GetHashCode - }, - { - Name: Level - }, - { - Name: Message - }, - { - Name: Timestamp - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Reports.ReportDataMarkdownFormatter, - Members: [ - { - Name: Write - } - ] - }, - { - Name: DecSm.Atom.Reports.ReportService, - Members: [ - { - Name: AddReportData - }, - { - Name: GetReportData - } - ] - }, - { - Name: DecSm.Atom.Reports.TableReportData, - Members: [ - { - Name: $ - }, - { - Name: BeforeStandardData - }, - { - Name: ColumnAlignments - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Header - }, - { - Name: Rows - }, - { - Name: Title - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Reports.TextReportData, - Members: [ - { - Name: $ - }, - { - Name: BeforeStandardData - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Text - }, - { - Name: Title - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Secrets.IDotnetUserSecrets - }, - { - Name: DecSm.Atom.Secrets.ISecretsProvider, - Members: [ - { - Name: GetSecret - } - ] - }, - { - Name: DecSm.Atom.SemVer, - Members: [ - { - Name: BuildNumberFromMetadata - }, - { - Name: BuildNumberFromPreRelease - }, - { - Name: CompareTo - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: ExtractBuildNumber - }, - { - Name: FromSystemVersion - }, - { - Name: GetHashCode - }, - { - Name: IsBetween - }, - { - Name: IsPreRelease - }, - { - Name: Major - }, - { - Name: Metadata - }, - { - Name: Minor - }, - { - Name: One - }, - { - Name: Parse - }, - { - Name: Parse - }, - { - Name: Parse - }, - { - Name: Patch - }, - { - Name: Prefix - }, - { - Name: PreRelease - }, - { - Name: ToString - }, - { - Name: ToSystemVersion - }, - { - Name: TryParse - }, - { - Name: TryParse - }, - { - Name: TryParse - }, - { - Name: TryParse - } - ] - }, - { - Name: DecSm.Atom.StepFailedException, - Members: [ - { - Name: ReportData - } - ] - }, - { - Name: DecSm.Atom.Util.BuildCache`1, - Members: [ - { - Name: Clear - }, - { - Name: Set - }, - { - Name: TryGetValue - } - ] - }, - { - Name: DecSm.Atom.Util.Scope.ActionScope, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Dispose - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: OnDispose - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Util.Scope.NullScope, - Members: [ - { - Name: Dispose - }, - { - Name: Instance - } - ] - }, - { - Name: DecSm.Atom.Util.Scope.TaskScope, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: DisposeAsync - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: OnDispose - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Util.ServiceStaticAccessor`1, - Members: [ - { - Name: Service - } - ] - }, - { - Name: DecSm.Atom.Util.StringUtil, - Members: [ - { - Name: GetLevenshteinDistance - }, - { - Name: SanitizeForLogging - }, - { - Name: SanitizeSecrets - } - ] - }, - { - Name: DecSm.Atom.Util.TaskExtensions, - Members: [ - { - Name: WithRetry - }, - { - Name: WithRetry - }, - { - Name: WithRetry - }, - { - Name: WithRetry - } - ] - }, - { - Name: DecSm.Atom.Util.TypeUtil, - Members: [ - { - Name: Convert - } - ] - }, - { - Name: DecSm.Atom.Variables.IVariablesHelper, - Members: [ - { - Name: WriteVariable - } - ] - }, - { - Name: DecSm.Atom.Variables.IWorkflowVariableProvider, - Members: [ - { - Name: ReadVariable - }, - { - Name: WriteVariable - } - ] - }, - { - Name: DecSm.Atom.Variables.IWorkflowVariableService, - Members: [ - { - Name: ReadVariable - }, - { - Name: WriteVariable - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.IWorkflowType, - Members: [ - { - Name: IsRunning - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.MatrixDimension, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Name - }, - { - Name: ToString - }, - { - Name: Values - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Options.IWorkflowOption, - Members: [ - { - Name: AllowMultiple - }, - { - Name: GetOptionsForCurrentTarget - }, - { - Name: Merge - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Options.ToggleWorkflowOption`1, - Members: [ - { - Name: $ - }, - { - Name: Disabled - }, - { - Name: Enabled - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: IsEnabled - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Options.WorkflowEnvironmentInjection, - Members: [ - { - Name: $ - }, - { - Name: AllowMultiple - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Options.WorkflowOption`2, - Members: [ - { - Name: $ - }, - { - Name: AllowMultiple - }, - { - Name: Create - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - }, - { - Name: Value - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Options.WorkflowParamInjection, - Members: [ - { - Name: $ - }, - { - Name: AllowMultiple - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: MergeWith - }, - { - Name: Name - }, - { - Name: ToString - }, - { - Name: Value - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Options.WorkflowSecretInjection, - Members: [ - { - Name: $ - }, - { - Name: AllowMultiple - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Options.WorkflowSecretsEnvironmentInjection, - Members: [ - { - Name: $ - }, - { - Name: AllowMultiple - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Options.WorkflowSecretsSecretInjection, - Members: [ - { - Name: $ - }, - { - Name: AllowMultiple - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Triggers.GithubScheduleTrigger, - Members: [ - { - Name: $ - }, - { - Name: CronExpression - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Triggers.GitPullRequestTrigger, - Members: [ - { - Name: $ - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: ExcludedBranches - }, - { - Name: ExcludedPaths - }, - { - Name: GetHashCode - }, - { - Name: IncludedBranches - }, - { - Name: IncludedPaths - }, - { - Name: IntoMain - }, - { - Name: ToString - }, - { - Name: Types - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Triggers.GitPushTrigger, - Members: [ - { - Name: $ - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: ExcludedBranches - }, - { - Name: ExcludedPaths - }, - { - Name: ExcludedTags - }, - { - Name: GetHashCode - }, - { - Name: IncludedBranches - }, - { - Name: IncludedPaths - }, - { - Name: IncludedTags - }, - { - Name: ToMain - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Triggers.IWorkflowTrigger - }, - { - Name: DecSm.Atom.Workflows.Definition.Triggers.ManualBoolInput, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: DefaultValue - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: ForParam - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Triggers.ManualChoiceInput, - Members: [ - { - Name: $ - }, - { - Name: Choices - }, - { - Name: Deconstruct - }, - { - Name: DefaultValue - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: ForParam - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Triggers.ManualInput, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Description - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Name - }, - { - Name: Required - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Triggers.ManualStringInput, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: DefaultValue - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: ForParam - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.Triggers.ManualTrigger, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Empty - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Inputs - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.WorkflowDefinition, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Name - }, - { - Name: Options - }, - { - Name: Targets - }, - { - Name: ToString - }, - { - Name: Triggers - }, - { - Name: WorkflowTypes - } - ] - }, - { - Name: DecSm.Atom.Workflows.Definition.WorkflowTargetDefinition, - Members: [ - { - Name: $ - }, - { - Name: CreateModel - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: MatrixDimensions - }, - { - Name: Name - }, - { - Name: Options - }, - { - Name: SuppressArtifactPublishing - }, - { - Name: ToString - }, - { - Name: WithMatrixDimensions - }, - { - Name: WithOptions - }, - { - Name: WithSuppressedArtifactPublishing - } - ] - }, - { - Name: DecSm.Atom.Workflows.IWorkflowOptionProvider, - Members: [ - { - Name: WorkflowOptions - } - ] - }, - { - Name: DecSm.Atom.Workflows.Model.WorkflowJobModel, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: JobDependencies - }, - { - Name: MatrixDimensions - }, - { - Name: Name - }, - { - Name: Options - }, - { - Name: Steps - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Model.WorkflowModel, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Jobs - }, - { - Name: Name - }, - { - Name: Options - }, - { - Name: ToString - }, - { - Name: Triggers - } - ] - }, - { - Name: DecSm.Atom.Workflows.Model.WorkflowStepModel, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: MatrixDimensions - }, - { - Name: Name - }, - { - Name: Options - }, - { - Name: SuppressArtifactPublishing - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Options.CustomStep, - Members: [ - { - Name: $ - }, - { - Name: AllowMultiple - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Name - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Options.DeployToEnvironment, - Members: [ - { - Name: $ - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Options.IJobRunsOn, - Members: [ - { - Name: JobRunsOn - }, - { - Name: MacOsLatestTag - }, - { - Name: UbuntuLatestTag - }, - { - Name: WindowsLatestTag - } - ] - }, - { - Name: DecSm.Atom.Workflows.Options.SetupDotnetStep, - Members: [ - { - Name: $ - }, - { - Name: Deconstruct - }, - { - Name: DotnetVersion - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: Equals - }, - { - Name: GetHashCode - }, - { - Name: Quality - }, - { - Name: ToString - } - ] - }, - { - Name: DecSm.Atom.Workflows.Writer.IWorkflowWriter, - Members: [ - { - Name: CheckForDirtyWorkflow - }, - { - Name: Generate - }, - { - Name: WorkflowType - } - ] - }, - { - Name: DecSm.Atom.Workflows.Writer.IWorkflowWriter`1 - }, - { - Name: DecSm.Atom.Workflows.Writer.WorkflowFileWriter`1, - Members: [ - { - Name: CheckForDirtyWorkflow - }, - { - Name: Generate - } - ] - } -] \ No newline at end of file diff --git a/DecSm.Atom.Tests/BuildTests/Core/CoreTests.cs b/DecSm.Atom.Tests/BuildTests/Core/CoreTests.cs deleted file mode 100644 index 4688171d..00000000 --- a/DecSm.Atom.Tests/BuildTests/Core/CoreTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace DecSm.Atom.Tests.BuildTests.Core; - -[TestFixture] -public class CoreTests -{ - [Test] - public void MinimalDefinition_IsEmpty() - { - // Arrange - var host = CreateTestHost(); - - // Act - var buildModel = host.Services.GetRequiredService(); - - // Assert - buildModel.ShouldSatisfyAllConditions(b => b.Targets.ShouldBeEmpty(), - b => b.TargetStates.ShouldBeEmpty(), - b => b.CurrentTarget.ShouldBeNull()); - } - - [Test] - public async Task DefaultBuildDefinition_HasDefaultTargets() - { - // Arrange - var host = CreateTestHost(); - - // Act - var buildModel = host.Services.GetRequiredService(); - - // Assert - await Verify(buildModel); - } -} diff --git a/DecSm.Atom.Tests/BuildTests/Core/MinimalAtomBuild.cs b/DecSm.Atom.Tests/BuildTests/Core/MinimalAtomBuild.cs deleted file mode 100644 index 758fa90f..00000000 --- a/DecSm.Atom.Tests/BuildTests/Core/MinimalAtomBuild.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace DecSm.Atom.Tests.BuildTests.Core; - -[BuildDefinition] -public sealed partial class MinimalAtomBuild : MinimalBuildDefinition; diff --git a/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowDependentTargetBuild.cs b/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowDependentTargetBuild.cs deleted file mode 100644 index 0c72a3ee..00000000 --- a/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowDependentTargetBuild.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace DecSm.Atom.Tests.BuildTests.Workflows; - -[BuildDefinition] -public partial class WorkflowDependentTargetBuild : MinimalBuildDefinition, - IWorkflowDependentTarget1, - IWorkflowDependentTarget2 -{ - public override IReadOnlyList Workflows => - [ - new("workflow-2") - { - Triggers = [new TestWorkflowTrigger()], - Targets = [WorkflowTargets.WorkflowTarget2], - Options = [new TestWorkflowOption()], - WorkflowTypes = [new TestWorkflowType()], - }, - ]; -} - -public interface IWorkflowDependentTarget1 -{ - Target WorkflowDependentTarget1 => - t => t - .DescribedAs("Workflow Target 1") - .Executes(() => Task.CompletedTask); -} - -public interface IWorkflowDependentTarget2 -{ - Target WorkflowTarget2 => - t => t - .DescribedAs("Workflow Target 2") - .DependsOn(nameof(IWorkflowDependentTarget1.WorkflowDependentTarget1)) - .Executes(() => Task.CompletedTask); -} diff --git a/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowSingleTargetBuild.cs b/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowSingleTargetBuild.cs deleted file mode 100644 index 96ade2eb..00000000 --- a/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowSingleTargetBuild.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace DecSm.Atom.Tests.BuildTests.Workflows; - -[BuildDefinition] -public partial class WorkflowSingleTargetBuild : MinimalBuildDefinition, IWorkflowSingleTarget -{ - public override IReadOnlyList Workflows => - [ - new("workflow-1") - { - Triggers = [new TestWorkflowTrigger()], - Targets = [WorkflowTargets.WorkflowSingleTarget], - Options = [new TestWorkflowOption()], - WorkflowTypes = [new TestWorkflowType()], - }, - ]; -} - -public interface IWorkflowSingleTarget -{ - Target WorkflowSingleTarget => - t => t - .DescribedAs("Workflow Target 1") - .Executes(() => Task.CompletedTask); -} diff --git a/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowTests.WorkflowDependentTargetBuild_GeneratesWorkflow.verified.txt b/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowTests.WorkflowDependentTargetBuild_GeneratesWorkflow.verified.txt deleted file mode 100644 index 40124f3e..00000000 --- a/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowTests.WorkflowDependentTargetBuild_GeneratesWorkflow.verified.txt +++ /dev/null @@ -1,36 +0,0 @@ -[ - { - Name: workflow-2, - Triggers: [ - {} - ], - Options: [ - { - AllowMultiple: false - } - ], - Jobs: [ - { - Name: WorkflowDependentTarget1, - Steps: [ - { - Name: WorkflowDependentTarget1, - SuppressArtifactPublishing: false - } - ] - }, - { - Name: WorkflowTarget2, - Steps: [ - { - Name: WorkflowTarget2, - SuppressArtifactPublishing: false - } - ], - JobDependencies: [ - WorkflowDependentTarget1 - ] - } - ] - } -] \ No newline at end of file diff --git a/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowTests.WorkflowSingleTargetBuild_GeneratesWorkflow.verified.txt b/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowTests.WorkflowSingleTargetBuild_GeneratesWorkflow.verified.txt deleted file mode 100644 index 45d55738..00000000 --- a/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowTests.WorkflowSingleTargetBuild_GeneratesWorkflow.verified.txt +++ /dev/null @@ -1,24 +0,0 @@ -[ - { - Name: workflow-1, - Triggers: [ - {} - ], - Options: [ - { - AllowMultiple: false - } - ], - Jobs: [ - { - Name: WorkflowSingleTarget, - Steps: [ - { - Name: WorkflowSingleTarget, - SuppressArtifactPublishing: false - } - ] - } - ] - } -] \ No newline at end of file diff --git a/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowTests.Workflows_WhenDirtyAndHeadless_RegeneratesWorkflows.verified.txt b/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowTests.Workflows_WhenDirtyAndHeadless_RegeneratesWorkflows.verified.txt deleted file mode 100644 index 17e4eaa2..00000000 --- a/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowTests.Workflows_WhenDirtyAndHeadless_RegeneratesWorkflows.verified.txt +++ /dev/null @@ -1,7 +0,0 @@ -00-00-00 +00:00 Atom: -00:00:00.000 ERR Stopped - - InvalidOperationException: One or more workflows are dirty. - Run 'atom -g' to regenerate them - at async void MoveNext() in AtomService.cs:112 - diff --git a/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowTests.cs b/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowTests.cs deleted file mode 100644 index bca43146..00000000 --- a/DecSm.Atom.Tests/BuildTests/Workflows/WorkflowTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace DecSm.Atom.Tests.BuildTests.Workflows; - -[TestFixture] -public class WorkflowTests -{ - [Test] - public async Task WorkflowSingleTargetBuild_GeneratesWorkflow() - { - // Arrange - var workflowWriter = new TestWorkflowWriter(); - - var host = CreateTestHost(configure: builder => - builder.Services.AddSingleton(workflowWriter)); - - // Act - await host.RunAsync(); - - // Assert - await Verify(workflowWriter.GeneratedWorkflows); - } - - [Test] - public async Task WorkflowDependentTargetBuild_GeneratesWorkflow() - { - // Arrange - var workflowWriter = new TestWorkflowWriter(); - - var host = CreateTestHost(configure: builder => - builder.Services.AddSingleton(workflowWriter)); - - // Act - await host.RunAsync(); - - // Assert - await Verify(workflowWriter.GeneratedWorkflows); - } - - [Test] - public async Task Workflows_WhenDirtyAndHeadless_RegeneratesWorkflows() - { - // Arrange - var console = new TestConsole(); - - var workflowWriter = new TestWorkflowWriter - { - IsDirty = true, - }; - - var host = CreateTestHost(console, - commandLineArgs: new(true, [new HeadlessArg()]), - configure: builder => builder.Services.AddSingleton(workflowWriter)); - - // Act - await host.RunAsync(); - - // Assert - await Verify(ConsoleOutputUtils.SanitizeLogDateTime(console.Output)); - } -} diff --git a/DecSm.Atom.Tests/ClassTests/Build/DefaultBuildVersionProviderTests.cs b/DecSm.Atom.Tests/ClassTests/Build/DefaultBuildVersionProviderTests.cs deleted file mode 100644 index 4f1edfcf..00000000 --- a/DecSm.Atom.Tests/ClassTests/Build/DefaultBuildVersionProviderTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -namespace DecSm.Atom.Tests.ClassTests.Build; - -[TestFixture] -public class DefaultBuildVersionProviderTests -{ - private static readonly string OsAgnosticRoot = OperatingSystem.IsWindows() - ? @"C:\Solution" - : "/Solution"; - - private static readonly char Ps = Path.DirectorySeparatorChar; - - private static AtomFileSystem NewFileSystem(IFileSystem fileSystem) - { - var result = new AtomFileSystem(A.Fake>()) - { - FileSystem = fileSystem, - PathLocators = [], - ProjectName = "Atom", - }; - - return result; - } - - [Test] - public void Version_Returns_VersionInfo() - { - const string directoryBuildProps = """ - - - 1.2.3 - - - """; - - // Arrange - var fileSystem = NewFileSystem(new MockFileSystem(new Dictionary - { - { $"{OsAgnosticRoot}{Ps}Solution.sln", new("") }, - { $"{OsAgnosticRoot}{Ps}Project", new MockDirectoryData() }, - { $"{OsAgnosticRoot}{Ps}Directory.Build.props", new(directoryBuildProps) }, - }, - OsAgnosticRoot)); - - var provider = new DefaultBuildVersionProvider(fileSystem); - - // Act - var version = provider.Version; - - // Assert - version - .ShouldNotBeNull() - .ShouldSatisfyAllConditions(x => x - .ToString() - .ShouldBe("1.2.3")); - } - - [Test] - [NonParallelizable] - [SuppressMessage("ReSharper", "MoveLocalFunctionAfterJumpStatement")] - public void Version_WhenDirectoryBuildPropsDoesNotExist_ReturnsDefaultVersion() - { - // Arrange - - var fileSystem = NewFileSystem(new MockFileSystem(new Dictionary - { - { $"{OsAgnosticRoot}{Ps}Solution.sln", new("") }, - { $"{OsAgnosticRoot}{Ps}Project", new MockDirectoryData() }, - }, - OsAgnosticRoot)); - - var provider = new DefaultBuildVersionProvider(fileSystem); - - // Act - var version = provider.Version; - - // Assert - version.ShouldBe(SemVer.Parse("1.0.0")); - } -} diff --git a/DecSm.Atom.Tests/ClassTests/Build/Definition/TargetDefinitionTests.cs b/DecSm.Atom.Tests/ClassTests/Build/Definition/TargetDefinitionTests.cs deleted file mode 100644 index c310470e..00000000 --- a/DecSm.Atom.Tests/ClassTests/Build/Definition/TargetDefinitionTests.cs +++ /dev/null @@ -1,213 +0,0 @@ -namespace DecSm.Atom.Tests.ClassTests.Build.Definition; - -[TestFixture] -public class TargetDefinitionTests -{ - [Test] - public void WithDescription_SetsDescription() - { - // Arrange - const string description = "description"; - - var targetDefinition = new TargetDefinition - { - Name = "name", - }; - - // Act - targetDefinition.DescribedAs(description); - - // Assert - targetDefinition.Description.ShouldBe(description); - } - - [Test] - public void Executes_SetsSingleTask() - { - // Arrange - var task = Task.CompletedTask; - - var targetDefinition = new TargetDefinition - { - Name = "name", - }; - - // Act - targetDefinition.Executes(() => task); - - // Assert - targetDefinition.Tasks.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), - x => x.Count.ShouldBe(1), - x => x[0](CancellationToken.None) - .ShouldBe(task)); - } - - [Test] - public void Executes_SetsMultipleTasks() - { - // Arrange - var task1 = Task.CompletedTask; - var task2 = Task.Delay(1); - - var targetDefinition = new TargetDefinition - { - Name = "name", - }; - - // Act - targetDefinition - .Executes(() => task1) - .Executes(() => task2); - - // Assert - targetDefinition.Tasks.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), - x => x.Count.ShouldBe(2), - x => x[0](CancellationToken.None) - .ShouldBe(task1), - x => x[1](CancellationToken.None) - .ShouldBe(task2)); - } - - // ReSharper disable once ClassNeverInstantiated.Local - private interface ITestTarget : IBuildDefinition - { - // ReSharper disable once UnusedMember.Local -#pragma warning disable CA1822 - Target TestTarget => x => x; -#pragma warning restore CA1822 - } - - [Test] - public void DependsOn_AddsDependency() - { - // Arrange - var targetDefinition = new TargetDefinition - { - Name = "name", - }; - - // Act - targetDefinition.DependsOn(nameof(ITestTarget.TestTarget)); - - // Assert - targetDefinition.Dependencies.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), - x => x.Count.ShouldBe(1), - x => x[0] - .ShouldBe(nameof(ITestTarget.TestTarget))); - } - - [Test] - public void RequiresParam_AddsRequiredParam() - { - // Arrange - const string paramName = "ParamName"; - - var targetDefinition = new TargetDefinition - { - Name = "name", - }; - - // Act - targetDefinition.RequiresParam(paramName); - - // Assert - targetDefinition.Params.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), - x => x.Count.ShouldBe(1), - x => x[0] - .Param - .ShouldBe(paramName)); - } - - [Test] - public void ProducesArtifact_AddsProducedArtifact() - { - // Arrange - const string artifactName = "ArtifactName"; - - var targetDefinition = new TargetDefinition - { - Name = "name", - }; - - // Act - targetDefinition.ProducesArtifact(artifactName); - - // Assert - targetDefinition.ProducedArtifacts.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), - x => x.Count.ShouldBe(1), - x => x[0] - .ArtifactName - .ShouldBe(artifactName)); - } - - [Test] - public void ConsumesArtifact_AddsConsumedArtifact() - { - // Arrange - const string artifactName = "ArtifactName"; - - var targetDefinition = new TargetDefinition - { - Name = "name", - }; - - // Act - targetDefinition.ConsumesArtifact(nameof(ITestTarget.TestTarget), artifactName); - - // Assert - targetDefinition.ConsumedArtifacts.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), - x => x.Count.ShouldBe(1), - x => x[0] - .ArtifactName - .ShouldBe(artifactName), - x => x[0] - .TargetName - .ShouldBe(nameof(ITestTarget.TestTarget))); - } - - [Test] - public void ProducesVariable_AddsProducedVariable() - { - // Arrange - const string variableName = "VariableName"; - - var targetDefinition = new TargetDefinition - { - Name = "name", - }; - - // Act - targetDefinition.ProducesVariable(variableName); - - // Assert - targetDefinition.ProducedVariables.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), - x => x.Count.ShouldBe(1), - x => x[0] - .ShouldBe(variableName)); - } - - [Test] - public void ConsumesVariable_AddsConsumedVariable() - { - // Arrange - const string variableName = "VariableName"; - - var targetDefinition = new TargetDefinition - { - Name = "name", - }; - - // Act - targetDefinition.ConsumesVariable(nameof(ITestTarget.TestTarget), variableName); - - // Assert - targetDefinition.ConsumedVariables.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), - x => x.Count.ShouldBe(1), - x => x[0] - .VariableName - .ShouldBe(variableName), - x => x[0] - .TargetName - .ShouldBe(nameof(ITestTarget.TestTarget))); - } -} diff --git a/DecSm.Atom.Tests/ClassTests/Paths/TransformFileScopeTests.cs b/DecSm.Atom.Tests/ClassTests/Paths/TransformFileScopeTests.cs deleted file mode 100644 index 0a890f35..00000000 --- a/DecSm.Atom.Tests/ClassTests/Paths/TransformFileScopeTests.cs +++ /dev/null @@ -1,104 +0,0 @@ -namespace DecSm.Atom.Tests.ClassTests.Paths; - -[TestFixture] -internal sealed class TransformFileScopeTests -{ - // Explicitly return IAtomFileSystem as it's better in this context -#pragma warning disable CA1859 - private static IAtomFileSystem CreateFileSystem( - IDictionary files, - string currentDirectory = "") => - new AtomFileSystem(A.Fake>()) - { - PathLocators = [], - FileSystem = new MockFileSystem(files, currentDirectory), - }; -#pragma warning restore CA1859 - - [Test] - public async Task CreateAsync_WhenFileDoesNotExist_CreatesFileAndWritesTransformedContent() - { - // Arrange - var fs = CreateFileSystem(new Dictionary()); - - // Act - await using var scope = await TransformFileScope.CreateAsync(fs.CreateRootedPath("file.txt"), _ => "test-text"); - - // Assert - (await fs.File.ReadAllTextAsync("file.txt")).ShouldBe("test-text"); - } - - [Test] - public async Task CreateAsync_WhenFileExists_OverwritesFileWithTransformedContent() - { - // Arrange - var fs = CreateFileSystem(new Dictionary - { - { "file.txt", new("existing-content") }, - }); - - // Act - await using var scope = await TransformFileScope.CreateAsync(fs.CreateRootedPath("file.txt"), _ => "test-text"); - - // Assert - (await fs.File.ReadAllTextAsync("file.txt")).ShouldBe("test-text"); - } - - [Test] - public async Task AddAsync_WhenScopeIsDisposed_ThrowsObjectDisposedException() - { - // Arrange - var fs = CreateFileSystem(new Dictionary - { - { "file.txt", new("existing-content") }, - }); - - var scope = await TransformFileScope.CreateAsync(fs.CreateRootedPath("file.txt"), _ => "test-text"); - - // Act - await scope - .DisposeAsync() - .ConfigureAwait(false); - - // Assert - Should.Throw(() => scope.AddAsync(_ => "test-text")); - } - - [Test] - public async Task AddAsync_AddsTransformationToExistingContent() - { - // Arrange - var fs = CreateFileSystem(new Dictionary - { - { "file.txt", new("existing-content") }, - }); - - await using var scope = await TransformFileScope.CreateAsync(fs.CreateRootedPath("file.txt"), _ => "test-text"); - - // Act - await scope.AddAsync(c => $"{c}-additional-text"); - - // Assert - (await fs.File.ReadAllTextAsync("file.txt")).ShouldBe("test-text-additional-text"); - } - - [Test] - public void CreateAndRestore_ResetsFileContentToOriginal() - { - // Arrange - var fs = CreateFileSystem(new Dictionary - { - { "file.txt", new("existing-content") }, - }); - - // Act - var scope = TransformFileScope.Create(fs.CreateRootedPath("file.txt"), _ => "test-text"); - scope.Dispose(); - - // Assert - fs - .File - .ReadAllText("file.txt") - .ShouldBe("existing-content"); - } -} diff --git a/DecSm.Atom.Tests/ClassTests/SemVerTests.cs b/DecSm.Atom.Tests/ClassTests/SemVerTests.cs deleted file mode 100644 index d0d1cd94..00000000 --- a/DecSm.Atom.Tests/ClassTests/SemVerTests.cs +++ /dev/null @@ -1,309 +0,0 @@ -namespace DecSm.Atom.Tests.ClassTests; - -[TestFixture] -internal sealed class SemVerTests -{ - [TestCase("1.0.0-alpha", true)] - [TestCase("1.0.0", false)] - public void IsPreRelease_ShouldReturnExpectedResult(string versionString, bool expectedResult) - { - var semVer = SemVer.Parse(versionString); - - semVer.IsPreRelease.ShouldBe(expectedResult); - } - - [TestCase("1.0.0", "2.0.0", "1.5.0", true)] - [TestCase("2.0.0", "1.0.0", "1.5.0", true)] - [TestCase("1.0.0", "2.0.0", "2.0.0", false)] - [TestCase("2.0.0", "1.0.0", "2.0.0", false)] - [TestCase("1.0.0", "1.0.0", "1.0.0", true)] - [TestCase("1.0.0", "1.0.0", "2.0.0", false)] - public void IsBetween_ShouldReturnExpectedResult( - string firstBound, - string secondBound, - string version, - bool expectedResult) - { - // Arrange - var first = SemVer.Parse(firstBound); - var second = SemVer.Parse(secondBound); - var semVer = SemVer.Parse(version); - - // Act - var result = semVer.IsBetween(first, second); - - // Assert - result.ShouldBe(expectedResult); - } - - [TestCase(1, 0, 0, 0, false, 1, 0, 0)] - [TestCase(1, 2, 3, 0, false, 1, 2, 3)] - [TestCase(1, 2, 3, 4, false, 1, 2, 3)] - [TestCase(1, 2, 3, 0, true, 1, 2, 3)] - public void FromSystemVersion_ShouldReturnExpectedSemVer( - int major, - int minor, - int build, - int revision, - bool throwIfContainsRevision, - int expectedMajor, - int expectedMinor, - int expectedPatch) - { - // Arrange - var version = new Version(major, minor, build, revision); - - // Act - var result = SemVer.FromSystemVersion(version, throwIfContainsRevision); - - // Assert - result.Major.ShouldBe(expectedMajor); - result.Minor.ShouldBe(expectedMinor); - result.Patch.ShouldBe(expectedPatch); - } - - [TestCase(1, 2, 3, 4, true)] - public void FromSystemVersion_WithRevision_ShouldThrowArgumentException( - int major, - int minor, - int build, - int revision, - bool throwIfContainsRevision) - { - // Arrange - var version = new Version(major, minor, build, revision); - - // Act / Assert - Should.Throw(() => SemVer.FromSystemVersion(version, throwIfContainsRevision)); - } - - [TestCase("1.0.0", 1, 0, 0)] - [TestCase("2.1.0", 2, 1, 0)] - [TestCase("3.2.1", 3, 2, 1)] - [TestCase("1.0.0-alpha", 1, 0, 0, "alpha", null)] - [TestCase("1.0.0+build", 1, 0, 0, null, "build")] - [TestCase("1.0.0-alpha+build", 1, 0, 0, "alpha", "build")] - public void Parse_ValidVersionString_ShouldReturnSemVer( - string versionString, - int expectedMajor, - int expectedMinor, - int expectedPatch, - string? expectedPreRelease = null, - string? expectedMetadata = null) - { - var semVer = SemVer.Parse(versionString); - - semVer.ShouldSatisfyAllConditions(() => semVer.Major.ShouldBe(expectedMajor), - () => semVer.Minor.ShouldBe(expectedMinor), - () => semVer.Patch.ShouldBe(expectedPatch), - () => semVer.PreRelease.ShouldBe(expectedPreRelease), - () => semVer.Metadata.ShouldBe(expectedMetadata)); - } - - [TestCase("1.0")] - [TestCase("1.0.0.0")] - [TestCase("1..0")] - [TestCase("1.0.0-")] - [TestCase("1.0.0+")] - [TestCase("-1.0.0")] - [TestCase("1.-1.0")] - [TestCase("1.1.-1")] - [TestCase("a.0.0")] - [TestCase("1.a.0")] - [TestCase("1.1.a")] - public void Parse_InvalidVersionString_ShouldThrowArgumentException(string versionString) => - Should.Throw(() => SemVer.Parse(versionString)); - - [TestCase("1.0.0", 1, 0, 0)] - [TestCase("2.1.0", 2, 1, 0)] - [TestCase("3.2.1", 3, 2, 1)] - [TestCase("1.0.0-alpha", 1, 0, 0, "alpha", null)] - [TestCase("1.0.0+build", 1, 0, 0, null, "build")] - [TestCase("1.0.0-alpha+build", 1, 0, 0, "alpha", "build")] - public void Parse_ValidVersionSpan_ShouldReturnSemVer( - string versionString, - int expectedMajor, - int expectedMinor, - int expectedPatch, - string? expectedPreRelease = null, - string? expectedMetadata = null) - { - var semVer = SemVer.Parse(versionString.AsSpan(), null); - - semVer.ShouldSatisfyAllConditions(() => semVer.Major.ShouldBe(expectedMajor), - () => semVer.Minor.ShouldBe(expectedMinor), - () => semVer.Patch.ShouldBe(expectedPatch), - () => semVer.PreRelease.ShouldBe(expectedPreRelease), - () => semVer.Metadata.ShouldBe(expectedMetadata)); - } - - [TestCase("1.0")] - [TestCase("1.0.0.0")] - [TestCase("1..0")] - [TestCase("1.0.0-")] - [TestCase("1.0.0+")] - public void Parse_InvalidVersionSpan_ShouldThrowArgumentException(string versionString) => - Should.Throw(() => SemVer.Parse(versionString.AsSpan(), null)); - - [TestCase("1.0.0", true)] - [TestCase("1.0", false)] - public void TryParse_ValidVersionString_ShouldReturnTrue(string versionString, bool expectedResult) => - SemVer - .TryParse(versionString, null, out _) - .ShouldBe(expectedResult); - - [TestCase("1.0.0", "1.0.0", 0)] - [TestCase("1.0.0", "2.0.0", -1)] - [TestCase("2.0.0", "1.0.0", 1)] - [TestCase("1.0.0-alpha", "1.0.0-beta", -1)] - [TestCase("1.0.0-beta", "1.0.0-alpha", 1)] - public void CompareTo_ShouldReturnExpectedResult(string version1, string version2, int expectedResult) - { - var semVer1 = SemVer.Parse(version1); - var semVer2 = SemVer.Parse(version2); - - semVer1 - .CompareTo(semVer2) - .ShouldBe(expectedResult); - } - - [TestCase("2.0.0", "1.0.0", true)] - [TestCase("1.0.0", "2.0.0", false)] - public void OperatorGreaterThan_ShouldReturnExpectedResult(string version1, string version2, bool expectedResult) - { - var semVer1 = SemVer.Parse(version1); - var semVer2 = SemVer.Parse(version2); - - (semVer1 > semVer2).ShouldBe(expectedResult); - } - - [TestCase("1.0.0", "2.0.0", true)] - [TestCase("2.0.0", "1.0.0", false)] - public void OperatorLessThan_ShouldReturnExpectedResult(string version1, string version2, bool expectedResult) - { - var semVer1 = SemVer.Parse(version1); - var semVer2 = SemVer.Parse(version2); - - (semVer1 < semVer2).ShouldBe(expectedResult); - } - - [TestCase("1.0.0", "1.0.0", true)] - [TestCase("2.0.0", "1.0.0", true)] - [TestCase("1.0.0", "2.0.0", false)] - public void OperatorGreaterThanOrEqual_ShouldReturnExpectedResult( - string version1, - string version2, - bool expectedResult) - { - var semVer1 = SemVer.Parse(version1); - var semVer2 = SemVer.Parse(version2); - - (semVer1 >= semVer2).ShouldBe(expectedResult); - } - - [TestCase("1.0.0", "1.0.0", true)] - [TestCase("1.0.0", "2.0.0", true)] - [TestCase("2.0.0", "1.0.0", false)] - public void OperatorLessThanOrEqual_ShouldReturnExpectedResult( - string version1, - string version2, - bool expectedResult) - { - var semVer1 = SemVer.Parse(version1); - var semVer2 = SemVer.Parse(version2); - - (semVer1 <= semVer2).ShouldBe(expectedResult); - } - - [TestCase("1.0.0", "1.0.0")] - [TestCase("1.0.0-alpha", "1.0.0-alpha")] - [TestCase("1.0.0+build", "1.0.0+build")] - [TestCase("1.0.0-alpha+build", "1.0.0-alpha+build")] - public void ToString_ShouldReturnCorrectFormat(string versionString, string expectedString) - { - var semVer = SemVer.Parse(versionString); - - semVer - .ToString() - .ShouldBe(expectedString); - } - - [TestCase("1.0.0", 1, 0, 0)] - [TestCase("1.0.0-alpha", 1, 0, 0, "alpha", null)] - [TestCase("1.0.0+build", 1, 0, 0, null, "build")] - [TestCase("1.0.0-alpha+build", 1, 0, 0, "alpha", "build")] - public void ImplicitConversionFromString_ShouldReturnSemVer( - string versionString, - int expectedMajor, - int expectedMinor, - int expectedPatch, - string? expectedPreRelease = null, - string? expectedMetadata = null) - { - SemVer semVer = versionString; - - semVer.ShouldSatisfyAllConditions(() => semVer.Major.ShouldBe(expectedMajor), - () => semVer.Minor.ShouldBe(expectedMinor), - () => semVer.Patch.ShouldBe(expectedPatch), - () => semVer.PreRelease.ShouldBe(expectedPreRelease), - () => semVer.Metadata.ShouldBe(expectedMetadata)); - } - - [TestCase("1.0.0", "1.0.0")] - [TestCase("1.0.0-alpha", "1.0.0-alpha")] - [TestCase("1.0.0+build", "1.0.0+build")] - [TestCase("1.0.0-alpha+build", "1.0.0-alpha+build")] - public void ImplicitConversionToString_ShouldReturnString(string versionString, string expectedString) - { - SemVer semVer = versionString; - - string actualString = semVer; - - actualString.ShouldBe(expectedString); - } - - [TestCase("1.0.0", "1.0.0")] - [TestCase("1.0.0-alpha", "1.0.0-alpha")] - [TestCase("1.0.0+build", "1.0.0+build")] - [TestCase("1.0.0-alpha+build", "1.0.0-alpha+build")] - public void Serialize_Deserialize_Json(string versionString, string expectedString) - { - var semVer = SemVer.Parse(versionString); - - var json = JsonSerializer.Serialize(semVer, JsonSerializerOptions.Default); - var actual = JsonSerializer.Deserialize(json, JsonSerializerOptions.Default); - - actual - ?.ToString() - .ShouldBe(expectedString); - } - - [TestCase("alpha.beta.1", 1)] - [TestCase("alpha.1", 1)] - [TestCase("alpha3.valid", 3)] - [TestCase("alpha.4valid", 4)] - [TestCase("rc.1", 1)] - [TestCase("alpha.1227", 1227)] - [TestCase("7A.is.legal", 7)] - [TestCase("SNAPSHOT-123", 123)] - [TestCase("---RC-SNAPSHOT.12--N-A", 12)] - [TestCase("prerelease", 0)] - [TestCase("alpha", 0)] - [TestCase("beta", 0)] - [TestCase("alpha.beta", 0)] - [TestCase("alpha-a.b-c-somethinglong", 0)] - [TestCase("beta", 0)] - [TestCase("DEV-SNAPSHOT", 0)] - [TestCase("alpha", 0)] - [TestCase("alpha3.4valid", 0)] - [TestCase("3alpha.4valid.1", 0)] - [TestCase("---RC-SNAPSHOT.12.9.1--.12", 0)] - [TestCase("---R-S.12.9.1--.12", 0)] - [TestCase("---RC-SNAPSHOT.12.9.1--.12", 0)] - public void DefaultBuildNumberExtractionStrategy_ExtractsBuildNumber(string preRelease, int expected) - { - var actual = SemVer.ExtractBuildNumber(preRelease); - - actual.ShouldBe(expected); - } -} diff --git a/DecSm.Atom.Tests/ClassTests/Util/TaskExtensionsTests.cs b/DecSm.Atom.Tests/ClassTests/Util/TaskExtensionsTests.cs deleted file mode 100644 index ce7fccae..00000000 --- a/DecSm.Atom.Tests/ClassTests/Util/TaskExtensionsTests.cs +++ /dev/null @@ -1,103 +0,0 @@ -namespace DecSm.Atom.Tests.ClassTests.Util; - -[TestFixture] -public class TaskExtensionsTests -{ - [Test] - public async Task WithRetry_NullTask_ReturnsCompletedTask() - { - // Arrange - Task? task = null; - - // Act & Assert - await task.WithRetry(); - } - - [Test] - public async Task WithRetry_SuccessfulTask_CompletesWithoutException() - { - // Arrange - var task = Task.CompletedTask; - - // Act & Assert - await task.WithRetry(3, TimeSpan.FromMilliseconds(1)); - } - - [Test] - public void WithRetry_NegativeRetryCount_ThrowsArgumentOutOfRangeException() - { - // Arrange - var task = Task.CompletedTask; - - // Act - var ex = Should.Throw(() => task.WithRetry(-1)); - - // Assert - ex.ParamName.ShouldBe("retryCount"); - } - - [Test] - public async Task WithRetry_FaultedTask_WithZeroRetries_ThrowsOriginalException() - { - // Arrange - var original = new InvalidOperationException("boom"); - var task = Task.FromException(original); - - // Act - var ex = await Should.ThrowAsync(async () => - await task.WithRetry(0, TimeSpan.FromMilliseconds(1))); - - // Assert - ex.Message.ShouldBe("boom"); - } - - [Test] - public async Task WithRetry_FaultedTask_WithRetries_ThrowsAggregateWithExpectedInnerCount() - { - // Arrange - var task = Task.FromException(new InvalidOperationException("boom")); - const int retryCount = 2; // total attempts = retryCount + 1 = 3 - - // Act - var aggregate = await Should.ThrowAsync(async () => - await task.WithRetry(retryCount, TimeSpan.FromMilliseconds(1))); - - // Assert - aggregate.InnerExceptions.Count.ShouldBe(retryCount + 1); - - aggregate - .InnerExceptions - .All(e => e is InvalidOperationException) - .ShouldBeTrue(); - } - - [Test] - public async Task WithRetry_CanceledTask_IsNotRetried_ThrowsTaskCanceledImmediately() - { - // Arrange - using var cts = new CancellationTokenSource(); - await cts.CancelAsync(); - var task = Task.FromCanceled(cts.Token); - - // Act - var ex = await Should.ThrowAsync(async () => - await task.WithRetry(3, TimeSpan.FromMilliseconds(25))); - - // Assert - ex.ShouldNotBeNull(); - } - - [Test] - public async Task WithRetry_OperationCanceledException_IsRethrown() - { - // Arrange - var task = Task.Run(() => throw new OperationCanceledException()); - - // Act - var ex = await Should.ThrowAsync(async () => - await task.WithRetry(3, TimeSpan.FromMilliseconds(25))); - - // Assert - ex.ShouldNotBeNull(); - } -} diff --git a/DecSm.Atom.Tests/_usings.cs b/DecSm.Atom.Tests/_usings.cs deleted file mode 100644 index dcf00151..00000000 --- a/DecSm.Atom.Tests/_usings.cs +++ /dev/null @@ -1,43 +0,0 @@ -global using System.Diagnostics; -global using System.Diagnostics.CodeAnalysis; -global using System.IO.Abstractions; -global using System.IO.Abstractions.TestingHelpers; -global using System.Linq.Expressions; -global using System.Reflection; -global using System.Runtime.CompilerServices; -global using System.Text; -global using System.Text.Json; -global using DecSm.Atom.Args; -global using DecSm.Atom.Build; -global using DecSm.Atom.Build.Definition; -global using DecSm.Atom.Build.Model; -global using DecSm.Atom.BuildInfo; -global using DecSm.Atom.Help; -global using DecSm.Atom.Hosting; -global using DecSm.Atom.Logging; -global using DecSm.Atom.Params; -global using DecSm.Atom.Paths; -global using DecSm.Atom.Process; -global using DecSm.Atom.Reports; -global using DecSm.Atom.Secrets; -global using DecSm.Atom.Tests.BuildTests.Core; -global using DecSm.Atom.TestUtils; -global using DecSm.Atom.Util; -global using DecSm.Atom.Util.Scope; -global using DecSm.Atom.Variables; -global using DecSm.Atom.Workflows; -global using DecSm.Atom.Workflows.Definition; -global using DecSm.Atom.Workflows.Writer; -global using DiffEngine; -global using FakeItEasy; -global using JetBrains.Annotations; -global using Microsoft.Extensions.Configuration; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Hosting; -global using Microsoft.Extensions.Logging; -global using NUnit.Framework; -global using Shouldly; -global using Spectre.Console; -global using Spectre.Console.Testing; -global using static DecSm.Atom.TestUtils.TestUtils; -global using Target = DecSm.Atom.Build.Definition.Target; diff --git a/DecSm.Atom.Tool.Tests/RunCommandTests.cs b/DecSm.Atom.Tool.Tests/RunCommandTests.cs deleted file mode 100644 index c2b08b6b..00000000 --- a/DecSm.Atom.Tool.Tests/RunCommandTests.cs +++ /dev/null @@ -1,92 +0,0 @@ -namespace DecSm.Atom.Tool.Tests; - -[TestFixture] -public class RunCommandTests -{ - [SetUp] - public void SetUp() - { - _fs = new(); - RunCommand.FileSystem = _fs; - RunCommand.MockDotnetCli = true; - } - - private MockFileSystem _fs = null!; - - private static string GetRoot() => - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? @"C:\" - : "/"; - - [Test] - public async Task Handle_ShouldFindProjectInParent_AndStopAtRootMarker() - { - // Arrange - var root = GetRoot(); - var repoDir = _fs.Path.Combine(root, "Repo"); - var subDir = _fs.Path.Combine(repoDir, "SubFolder"); - var targetDir = _fs.Path.Combine(subDir, "Target"); - - // CRITICAL FIX: Explicitly create the full path - // MockFileSystem needs the physical directory to exist to enumerate it - _fs.AddDirectory(targetDir); - - // Adding the file automatically creates 'Repo', but not 'SubFolder\Target' - _fs.AddFile(_fs.Path.Combine(repoDir, "MyProj.csproj"), new("")); - - // Add the root marker - _fs.AddDirectory(_fs.Path.Combine(subDir, ".git")); - - _fs.Directory.SetCurrentDirectory(targetDir); - - // Act - var result = await RunCommand.Handle([], "MyProj", CancellationToken.None); - - // Assert - result.ShouldBe(1); - } - - [Test] - public async Task Handle_ShouldFindNestedProject_WhenConventionSearchIsEnabled() - { - // Arrange - var root = GetRoot(); - var workDir = _fs.Path.Combine(root, "Work"); - var nestedProject = _fs.Path.Combine(workDir, "Atom", "Atom.csproj"); - - // AddDirectory is not strictly needed here because AddFile creates the parent - _fs.AddFile(nestedProject, new("")); - _fs.Directory.SetCurrentDirectory(workDir); - - // Act - var result = await RunCommand.Handle([], "Atom", CancellationToken.None); - - // Assert - result.ShouldBe(0); - } - - [Test] - public async Task Handle_ShouldPrioritizeBreadthFirst_InDownwardSearch() - { - // Arrange - var root = GetRoot(); - var searchRoot = _fs.Path.Combine(root, "SearchRoot"); - - var deepPath = _fs.Path.Combine(searchRoot, "Level1", "Level2", "Target.csproj"); - var shallowPath = _fs.Path.Combine(searchRoot, "Level1_Sibling", "Target.csproj"); - - // AddFile creates all necessary parent directories for these files - _fs.AddFile(deepPath, new("")); - _fs.AddFile(shallowPath, new("")); - - // Ensure the search root itself is initialized - _fs.AddDirectory(searchRoot); - _fs.Directory.SetCurrentDirectory(searchRoot); - - // Act - var result = await RunCommand.Handle([], "Target", CancellationToken.None); - - // Assert - result.ShouldBe(0); - } -} diff --git a/DecSm.Atom.Tool.Tests/_usings.cs b/DecSm.Atom.Tool.Tests/_usings.cs deleted file mode 100644 index 81e7008e..00000000 --- a/DecSm.Atom.Tool.Tests/_usings.cs +++ /dev/null @@ -1,4 +0,0 @@ -global using System.IO.Abstractions.TestingHelpers; -global using System.Runtime.InteropServices; -global using DecSm.Atom.Tool.Commands; -global using Shouldly; diff --git a/DecSm.Atom.slnx b/DecSm.Atom.slnx deleted file mode 100644 index c4636609..00000000 --- a/DecSm.Atom.slnx +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DecSm.Atom/Build/Definition/BuildDefinition.cs b/DecSm.Atom/Build/Definition/BuildDefinition.cs deleted file mode 100644 index 55731d88..00000000 --- a/DecSm.Atom/Build/Definition/BuildDefinition.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace DecSm.Atom.Build.Definition; - -/// -/// A comprehensive base class for creating build definitions, pre-configuring a rich set of common build targets and -/// options. -/// -/// -/// -/// This class is the recommended starting point for most Atom build projects. It implements several standard -/// interfaces, -/// providing targets for common operations such as setting up build info (), -/// validating the build (), and managing .NET user secrets ( -/// ). -/// -/// -/// A project's main build definition class typically inherits from this class. -/// -/// -/// -/// A typical build definition class: -/// -/// [BuildDefinition] -/// [GenerateEntryPoint] -/// internal partial class Build : BuildDefinition, IMyTargets -/// { -/// // ... -/// } -/// -/// -/// -/// -[PublicAPI] -public abstract class BuildDefinition(IServiceProvider services) - : MinimalBuildDefinition(services), ISetupBuildInfo, IDotnetUserSecrets; diff --git a/DecSm.Atom/Build/Definition/MinimalBuildDefinition.cs b/DecSm.Atom/Build/Definition/MinimalBuildDefinition.cs deleted file mode 100644 index 75719abb..00000000 --- a/DecSm.Atom/Build/Definition/MinimalBuildDefinition.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace DecSm.Atom.Build.Definition; - -/// -/// A minimal abstract base class for creating build definitions, providing default implementations for -/// . -/// -/// -/// -/// While developers should typically inherit from the more comprehensive , -/// this class can be used for a leaner setup. -/// -/// -/// The and properties are populated by source -/// generators based on the interfaces and attributes used in the derived class. A derived class must be -/// decorated with . -/// -/// -/// -/// A minimal build definition: -/// -/// [BuildDefinition] -/// internal partial class MyBuild : MinimalBuildDefinition, IMyTargets -/// { -/// // ... -/// } -/// -/// -/// The service provider for dependency injection. -[PublicAPI] -public abstract class MinimalBuildDefinition(IServiceProvider services) : IBuildDefinition -{ - /// - /// Provides access to the service provider associated with the build definition. - /// This property allows resolving dependencies and accessing services registered in the application. - /// - public IServiceProvider Services => services; - - /// - public abstract IReadOnlyDictionary TargetDefinitions { get; } - - /// - public abstract IReadOnlyDictionary ParamDefinitions { get; } - - /// - public abstract object? AccessParam(string paramName); - - /// - public virtual IReadOnlyList Workflows => []; - - /// - public virtual IReadOnlyList GlobalWorkflowOptions => []; -} diff --git a/DecSm.Atom/DecSm.Atom.props b/DecSm.Atom/DecSm.Atom.props deleted file mode 100644 index 565c872b..00000000 --- a/DecSm.Atom/DecSm.Atom.props +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DecSm.Atom/DecSm.Atom.targets b/DecSm.Atom/DecSm.Atom.targets deleted file mode 100644 index 32236be5..00000000 --- a/DecSm.Atom/DecSm.Atom.targets +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/DecSm.Atom/Paths/AtomFileSystem.cs b/DecSm.Atom/Paths/AtomFileSystem.cs deleted file mode 100644 index 9733cf52..00000000 --- a/DecSm.Atom/Paths/AtomFileSystem.cs +++ /dev/null @@ -1,195 +0,0 @@ -namespace DecSm.Atom.Paths; - -/// -/// Defines a specialized file system abstraction for Atom, providing access to key build-related directories and -/// paths. -/// -[PublicAPI] -public interface IAtomFileSystem : IFileSystem -{ - /// - /// Gets the name of the project, typically derived from the entry assembly. - /// - string ProjectName { get; } - - /// - /// Gets a value indicating whether the application is file-based (e.g., *.cs) or project-based (e.g., *.csproj). - /// - bool IsFileBasedApp { get; } - - /// - /// Gets the underlying instance for general-purpose file operations. - /// - IFileSystem FileSystem { get; } - - /// - /// Gets the root directory of the Atom project, identified by markers like .git or .sln files. - /// - RootedPath AtomRootDirectory => GetPath(AtomPaths.Root); - - /// - /// Gets the default directory for storing build artifacts. - /// - RootedPath AtomArtifactsDirectory => GetPath(AtomPaths.Artifacts); - - /// - /// Gets the default directory for publishing final build outputs. - /// - RootedPath AtomPublishDirectory => GetPath(AtomPaths.Publish); - - /// - /// Gets the temporary directory for build operations. - /// - RootedPath AtomTempDirectory => GetPath(AtomPaths.Temp); - - /// - /// Gets the current working directory of the application. - /// - RootedPath CurrentDirectory => new(this, FileSystem.Directory.GetCurrentDirectory()); - - /// - IDirectory IFileSystem.Directory => FileSystem.Directory; - - /// - IDirectoryInfoFactory IFileSystem.DirectoryInfo => FileSystem.DirectoryInfo; - - /// - IDriveInfoFactory IFileSystem.DriveInfo => FileSystem.DriveInfo; - - /// - IFile IFileSystem.File => FileSystem.File; - - /// - IFileInfoFactory IFileSystem.FileInfo => FileSystem.FileInfo; - - /// - IFileStreamFactory IFileSystem.FileStream => FileSystem.FileStream; - - /// - IFileSystemWatcherFactory IFileSystem.FileSystemWatcher => FileSystem.FileSystemWatcher; - - /// - IPath IFileSystem.Path => FileSystem.Path; - - /// - IFileVersionInfoFactory IFileSystem.FileVersionInfo => FileSystem.FileVersionInfo; - - /// - /// Resolves a path by its key, using registered path providers and falling back to default logic. - /// - /// The key identifying the path (e.g., "Root", "Artifacts"). - /// A corresponding to the key. - RootedPath GetPath(string key); - - /// - /// Resolves the path for a given file marker type. - /// - /// The type of the file marker, which must implement . - /// A for the specified file marker. - RootedPath GetPath() - where T : IPathLocator => - T.Path(this); - - /// - /// Creates a new instance from a string path. - /// - /// The string representation of the path. - /// A new associated with this file system instance. - RootedPath CreateRootedPath(string path) => - new(this, path); -} - -/// -/// Internal implementation of . -/// -/// The logger for diagnostics. -internal sealed class AtomFileSystem(ILogger logger) : IAtomFileSystem -{ - private readonly AsyncLocal _getPathDepth = new(); - private readonly Dictionary _pathCache = []; - - public required IReadOnlyList PathLocators { private get; init; } - - public string ProjectName { get; init; } = Assembly.GetEntryAssembly()!.GetName() - .Name!; - - public bool IsFileBasedApp => AppContext.GetData("EntryPointFilePath") is string s && s.EndsWith(".cs"); - - public required IFileSystem FileSystem { get; init; } - - /// - public RootedPath GetPath(string key) - { - if (_getPathDepth.Value > 100) - throw new InvalidOperationException( - "Path resolution depth exceeded. It is likely that a circular dependency exists."); - - _getPathDepth.Value++; - - try - { - if (_pathCache.TryGetValue(key, out var path)) - { - logger.LogDebug("Path for key '{Key}' found in cache: {Path}", key, path); - - return path; - } - - path = PathLocators - .Select(x => x.GetPath(key)) - .FirstOrDefault(x => x is not null); - - if (path is not null) - { - logger.LogDebug("Path for key '{Key}' located: {Path}", key, path); - - return _pathCache[key] = path; - } - - var result = _pathCache[key] = key switch - { - AtomPaths.Root => GetRoot(), - AtomPaths.Artifacts or AtomPaths.Publish => GetPath(AtomPaths.Root) / "atom-publish", - AtomPaths.Temp => new(this, FileSystem.Path.GetTempPath()), - _ => throw new InvalidOperationException($"Could not locate path for key '{key}'"), - }; - - logger.LogDebug("Path for key '{Key}' computed: {Path}", key, result); - - return result; - } - finally - { - _getPathDepth.Value--; - } - } - - /// - /// Determines the root directory by traversing up from the current directory and looking for project markers. - /// - private RootedPath GetRoot() - { - var currentDir = ((IAtomFileSystem)this).CurrentDirectory; - - while (currentDir.Parent is not null) - { - currentDir = currentDir.Parent; - - if (FileSystem - .Directory - .EnumerateDirectories(currentDir, "*.git", SearchOption.TopDirectoryOnly) - .Any() || - FileSystem - .Directory - .EnumerateFiles(currentDir, "*.slnx", SearchOption.TopDirectoryOnly) - .Any() || - FileSystem - .Directory - .EnumerateFiles(currentDir, "*.sln", SearchOption.TopDirectoryOnly) - .Any()) - return currentDir; - } - - return ((IAtomFileSystem)this).CurrentDirectory; - } -} diff --git a/DecSm.Atom/Paths/AtomPaths.cs b/DecSm.Atom/Paths/AtomPaths.cs deleted file mode 100644 index 24abbb35..00000000 --- a/DecSm.Atom/Paths/AtomPaths.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace DecSm.Atom.Paths; - -/// -/// Provides constants for key directory paths and an extension method for path provider registration. -/// -[PublicAPI] -public static class AtomPaths -{ - /// - /// Represents the key for the root directory of the Atom project. - /// - /// - public const string Root = "Root"; - - /// - /// Represents the key for the directory where build artifacts are stored. - /// - /// - public const string Artifacts = "Artifacts"; - - /// - /// Represents the key for the directory where build outputs are published. - /// - /// - public const string Publish = "Publish"; - - /// - /// Represents the key for the temporary directory used during builds. - /// - /// - public const string Temp = "Temp"; - - extension(IServiceCollection services) - { - /// - /// Registers a custom path provider with the dependency injection container. - /// - /// A function that resolves a based on a key. - /// The priority of the provider. Higher values take precedence. - [PublicAPI] - public void ProvidePath(Func locate, int priority = 1) => - services.AddSingleton(new FunctionPathProvider - { - Priority = priority, - Resolver = locate, - }); - - /// - /// Registers a custom path provider with the dependency injection container. - /// - /// A function that resolves a based on a key. - /// The priority of the provider. Higher values take precedence. - [PublicAPI] - public void ProvidePath(Func locate, int priority = 1) => - services.AddSingleton(provider => new FunctionPathProvider - { - Priority = priority, - Resolver = key => locate(key, provider.GetRequiredService()), - }); - } -} diff --git a/DecSm.Atom/Paths/FunctionPathProvider.cs b/DecSm.Atom/Paths/FunctionPathProvider.cs deleted file mode 100644 index 8ffbbe01..00000000 --- a/DecSm.Atom/Paths/FunctionPathProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace DecSm.Atom.Paths; - -/// -/// A concrete implementation of that uses a delegate to locate paths. -/// -public sealed class FunctionPathProvider : IPathProvider -{ - /// - /// Gets the function that implements the path location logic. - /// - public required Func Resolver { get; init; } - - /// - public required int Priority { get; init; } - - /// - public RootedPath? GetPath(string key) => - Resolver(key); -} diff --git a/DecSm.Atom/Paths/IPathLocator.cs b/DecSm.Atom/Paths/IPathLocator.cs deleted file mode 100644 index 203f119f..00000000 --- a/DecSm.Atom/Paths/IPathLocator.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DecSm.Atom.Paths; - -/// -/// Defines a contract for types that represent a specific, well-known file path within the file system. -/// -public interface IPathLocator -{ - /// - /// Gets the rooted path of the file. - /// - /// The file system service to resolve the path against. - /// A representing the location of the file. - static abstract RootedPath Path(IAtomFileSystem fileSystem); -} diff --git a/DecSm.Atom/Paths/IPathProvider.cs b/DecSm.Atom/Paths/IPathProvider.cs deleted file mode 100644 index e33ee33e..00000000 --- a/DecSm.Atom/Paths/IPathProvider.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace DecSm.Atom.Paths; - -/// -/// Defines a provider for locating paths within the Atom file system. -/// -[PublicAPI] -public interface IPathProvider -{ - /// - /// Gets the priority of this provider. Providers with higher priority values are queried first. - /// - int Priority { get; } - - /// - /// Attempts to locate a path based on a given key. - /// - /// The key identifying the path to locate (e.g., "Root", "Artifacts"). - /// A if the provider can resolve the key; otherwise, null. - RootedPath? GetPath(string key); -} diff --git a/DecSm.Atom/Paths/RootedPath.cs b/DecSm.Atom/Paths/RootedPath.cs deleted file mode 100644 index 2c0199b5..00000000 --- a/DecSm.Atom/Paths/RootedPath.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace DecSm.Atom.Paths; - -/// -/// Represents a file system path that is rooted within a specific instance. -/// -/// The file system instance this path belongs to. -/// The absolute path string. -[PublicAPI] -public record RootedPath(IAtomFileSystem FileSystem, string Path) -{ - /// - /// Gets the parent directory of the current path. - /// - /// - /// A new representing the parent directory, or null if the current path is a - /// root. - /// - public RootedPath? Parent - { - get - { - if (FileSystem.Path.GetPathRoot(Path) == Path) - return null; - - var path = Path switch - { - [.., '/'] or [.., '\\'] => Path[..^1], - _ => Path, - }; - - var lastForwardSlash = path.LastIndexOf('/'); - var lastBackSlash = path.LastIndexOf('\\'); - - var lastSlash = Math.Max(lastForwardSlash, lastBackSlash); - - if (lastSlash == -1) - return null; - - return this with - { - Path = $"{path[..lastSlash]}{FileSystem.Path.DirectorySeparatorChar}", - }; - } - } - - /// - /// Indicates whether the path exists in the file system as either a file or a directory. - /// - public bool PathExists => FileExists || DirectoryExists; - - /// - /// Indicates whether the path exists in the file system as a file. - /// - public bool FileExists => FileSystem.File.Exists(Path); - - /// - /// Indicates whether the path exists in the file system as a directory. - /// - public bool DirectoryExists => FileSystem.Directory.Exists(Path); - - /// - /// Gets the file name from the current path. - /// - /// The file name including its extension, or null if the path does not represent an existing file. - public string? FileName => - FileExists - ? FileSystem.Path.GetFileName(Path) - : null; - - /// - /// Gets the file name of the current path without its extension. - /// - /// The file name without its extension. - public string FileNameWithoutExtension => FileSystem.Path.GetFileNameWithoutExtension(Path); - - /// - /// Gets the directory name of the current path. - /// - /// The directory name, or null if the path does not represent an existing directory. - public string? DirectoryName => - DirectoryExists - ? FileSystem.Path.GetDirectoryName(Path) - : null; - - /// - /// Combines the current with a string segment. - /// - /// The base . - /// The string segment to append. - /// A new representing the combined path. - public static RootedPath operator /(RootedPath left, string right) => - left with - { - Path = left.FileSystem.Path.Combine(left.Path, right), - }; - - /// - /// Implicitly converts a to its string representation. - /// - /// The to convert. - /// The string representation of the path. - public static implicit operator string(RootedPath path) => - path.Path; - - /// - public override string ToString() => - Path; -} diff --git a/DecSm.Atom/Paths/TransformFileScope.cs b/DecSm.Atom/Paths/TransformFileScope.cs deleted file mode 100644 index c6dc9be0..00000000 --- a/DecSm.Atom/Paths/TransformFileScope.cs +++ /dev/null @@ -1,196 +0,0 @@ -namespace DecSm.Atom.Paths; - -/// -/// Represents a disposable scope for performing temporary transformations on a file's content. -/// Upon disposal, the file's original content is restored unless restoration is explicitly canceled. -/// -/// -/// This is useful for temporarily modifying project files during a build without permanent changes. -/// -/// -/// -/// using (var scope = await TransformFileScope.CreateAsync(myFile, content => content + "\n<NewProperty>Value</NewProperty>")) -/// { -/// // The file is now modified. It will be restored when the scope is disposed. -/// } -/// -/// -[PublicAPI] -public sealed class TransformFileScope : IAsyncDisposable, IDisposable -{ - private readonly RootedPath _file; - private readonly string? _initialContent; - private bool _cancelled; - private bool _disposed; - - /// - /// Initializes a new instance of the class. - /// - /// The file to be managed. - /// The original content of the file before transformation. - private TransformFileScope(RootedPath file, string? initialContent) - { - _file = file; - _initialContent = initialContent; - } - - /// - /// Asynchronously disposes the scope and restores the file to its original content, unless canceled. - /// - public async ValueTask DisposeAsync() - { - if (_disposed) - return; - - _disposed = true; - - if (_cancelled) - return; - - // No cancellation token here - we'd prefer to wait for the file to write so it doesn't get mangled. - if (_initialContent is null) - _file.FileSystem.File.Delete(_file); - else - await _file.FileSystem.File.WriteAllTextAsync(_file, _initialContent); - } - - /// - /// Disposes the scope and restores the file to its original content, unless canceled. - /// - public void Dispose() - { - if (_disposed) - return; - - _disposed = true; - - if (_cancelled) - return; - - if (_initialContent is null) - _file.FileSystem.File.Delete(_file); - else - _file.FileSystem.File.WriteAllText(_file, _initialContent); - } - - /// - /// Creates a new for a file and applies an asynchronous transformation. - /// - /// The file to transform. - /// A function to apply to the file's content. - /// A cancellation token. - /// A new instance managing the transformed file. - public static async Task CreateAsync( - RootedPath file, - Func transform, - CancellationToken cancellationToken = default) - { - string? initialContent = null; - - if (!file.FileSystem.File.Exists(file)) - await file - .FileSystem - .File - .Create(file) - .DisposeAsync(); - else - initialContent = await file.FileSystem.File.ReadAllTextAsync(file, cancellationToken); - - var scope = new TransformFileScope(file, initialContent); - - try - { - await file.FileSystem.File.WriteAllTextAsync(file, - transform(initialContent ?? string.Empty), - cancellationToken); - } - catch (OperationCanceledException) - { - await scope.DisposeAsync(); - - throw; - } - - return scope; - } - - /// - /// Applies an additional asynchronous transformation to the file within the current scope. - /// - /// A function to apply to the file's current content. - /// A cancellation token. - /// The current instance. - public async Task AddAsync( - Func transform, - CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - if (_cancelled) - return this; - - try - { - var currentContent = await _file.FileSystem.File.ReadAllTextAsync(_file, cancellationToken); - await _file.FileSystem.File.WriteAllTextAsync(_file, transform(currentContent), cancellationToken); - - return this; - } - catch (OperationCanceledException) - { - await DisposeAsync(); - - throw; - } - } - - /// - /// Creates a new for a file and applies a synchronous transformation. - /// - /// The file to transform. - /// A function to apply to the file's content. - /// A new instance managing the transformed file. - public static TransformFileScope Create(RootedPath file, Func transform) - { - string? initialContent = null; - - if (!file.FileSystem.File.Exists(file)) - file - .FileSystem - .File - .Create(file) - .Dispose(); - else - initialContent = file.FileSystem.File.ReadAllText(file); - - var scope = new TransformFileScope(file, initialContent); - - file.FileSystem.File.WriteAllText(file, transform(initialContent ?? string.Empty)); - - return scope; - } - - /// - /// Applies an additional synchronous transformation to the file within the current scope. - /// - /// A function to apply to the file's current content. - /// The current instance. - public TransformFileScope Add(Func transform) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - if (_cancelled) - return this; - - var currentContent = _file.FileSystem.File.ReadAllText(_file); - _file.FileSystem.File.WriteAllText(_file, transform(currentContent)); - - return this; - } - - /// - /// Prevents the file from being restored to its original content upon disposal. - /// - public void CancelRestore() => - _cancelled = true; -} diff --git a/DecSm.Atom/Paths/TransformFileScopeExtensions.cs b/DecSm.Atom/Paths/TransformFileScopeExtensions.cs deleted file mode 100644 index 853d8b21..00000000 --- a/DecSm.Atom/Paths/TransformFileScopeExtensions.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace DecSm.Atom.Paths; - -/// -/// Provides extension methods for chaining asynchronous transformations on -/// and . -/// -[PublicAPI] -public static class TransformFileScopeExtensions -{ - /// - /// Applies an additional asynchronous transformation to each file within the scope returned by a task. - /// - /// A task that returns a . - /// A function to apply to each file's current content. - /// - /// A task that represents the completion of the transformation, yielding the same - /// instance. - /// - public static async Task AddAsync( - this Task scopeTask, - Func transform) => - await (await scopeTask).AddAsync(transform); - - /// - /// Applies an additional asynchronous transformation to the file within the scope returned by a task. - /// - /// A task that returns a . - /// A function to apply to the file's current content. - /// - /// A task that represents the completion of the transformation, yielding the same - /// instance. - /// - public static async Task AddAsync( - this Task scopeTask, - Func transform) => - await (await scopeTask).AddAsync(transform); -} diff --git a/DecSm.Atom/Paths/TransformMultiFileScope.cs b/DecSm.Atom/Paths/TransformMultiFileScope.cs deleted file mode 100644 index bd05c392..00000000 --- a/DecSm.Atom/Paths/TransformMultiFileScope.cs +++ /dev/null @@ -1,209 +0,0 @@ -namespace DecSm.Atom.Paths; - -/// -/// Represents a disposable scope for performing temporary transformations on multiple files. -/// Upon disposal, the original content of all files is restored unless restoration is explicitly canceled. -/// -[PublicAPI] -public sealed class TransformMultiFileScope : IAsyncDisposable, IDisposable -{ - private readonly IEnumerable _files; - private readonly string?[] _initialContents; - private bool _cancelled; - private bool _disposed; - - /// - /// Initializes a new instance of the class. - /// - /// The collection of files to be managed. - /// The original contents of the files before transformation. - private TransformMultiFileScope(IEnumerable files, string?[] initialContents) - { - _files = files; - _initialContents = initialContents; - } - - /// - /// Asynchronously disposes the scope and restores all managed files to their original content, unless canceled. - /// - public async ValueTask DisposeAsync() - { - if (_disposed) - return; - - _disposed = true; - - if (_cancelled) - return; - - await Task.WhenAll(_files.Select(async (x, i) => - { - if (_initialContents[i] is null) - x.FileSystem.File.Delete(x); - else - await x.FileSystem.File.WriteAllTextAsync(x, _initialContents[i]); - })); - } - - /// - /// Disposes the scope and restores all managed files to their original content, unless canceled. - /// - public void Dispose() - { - if (_disposed) - return; - - _disposed = true; - - if (_cancelled) - return; - - // No cancellation token here - we'd prefer to wait for the files to write so they don't get mangled. - foreach (var (file, i) in _files.Select((x, i) => (x, i))) - if (_initialContents[i] is null) - file.FileSystem.File.Delete(file); - else - file.FileSystem.File.WriteAllText(file, _initialContents[i]); - } - - /// - /// Creates a new for a collection of files and applies an asynchronous - /// transformation to each. - /// - /// The files to transform. - /// A function to apply to each file's content. - /// A cancellation token. - /// A new instance managing the transformed files. - public static async Task CreateAsync( - IEnumerable files, - Func transform, - CancellationToken cancellationToken = default) - { - var filesArray = files.ToArray(); - - var initialContents = await Task.WhenAll(filesArray.Select(async x => - { - if (x.FileSystem.File.Exists(x)) - return await x.FileSystem.File.ReadAllTextAsync(x, cancellationToken); - - await x - .FileSystem - .File - .Create(x) - .DisposeAsync(); - - return null; - })); - - var scope = new TransformMultiFileScope(filesArray, initialContents); - - try - { - await Task.WhenAll(filesArray.Select((x, i) => - x.FileSystem.File.WriteAllTextAsync(x, - transform(initialContents[i] ?? string.Empty), - cancellationToken))); - } - catch (OperationCanceledException) - { - await scope.DisposeAsync(); - } - - return scope; - } - - /// - /// Applies an additional asynchronous transformation to each file within the current scope. - /// - /// A function to apply to each file's current content. - /// A cancellation token. - /// The current instance. - public async Task AddAsync( - Func transform, - CancellationToken cancellationToken = default) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - if (_cancelled) - return this; - - try - { - await Task.WhenAll(_files.Select(async x => - { - var currentContent = await x.FileSystem.File.ReadAllTextAsync(x, cancellationToken); - await x.FileSystem.File.WriteAllTextAsync(x, transform(currentContent), cancellationToken); - })); - - return this; - } - catch (OperationCanceledException) - { - await DisposeAsync(); - - throw; - } - } - - /// - /// Creates a new for a collection of files and applies a synchronous - /// transformation to each. - /// - /// The files to transform. - /// A function to apply to each file's content. - /// A new instance managing the transformed files. - public static TransformMultiFileScope Create(IEnumerable files, Func transform) - { - var filesArray = files.ToArray(); - - var initialContents = filesArray - .Select(x => - { - if (x.FileSystem.File.Exists(x)) - return x.FileSystem.File.ReadAllText(x); - - x - .FileSystem - .File - .Create(x) - .Dispose(); - - return null; - }) - .ToArray(); - - var scope = new TransformMultiFileScope(filesArray, initialContents); - - foreach (var (file, i) in filesArray.Select((x, i) => (x, i))) - file.FileSystem.File.WriteAllText(file, transform(initialContents[i] ?? string.Empty)); - - return scope; - } - - /// - /// Applies an additional synchronous transformation to each file within the current scope. - /// - /// A function to apply to each file's current content. - /// The current instance. - public TransformMultiFileScope Add(Func transform) - { - ObjectDisposedException.ThrowIf(_disposed, this); - - if (_cancelled) - return this; - - foreach (var file in _files) - { - var currentContent = file.FileSystem.File.ReadAllText(file); - file.FileSystem.File.WriteAllText(file, transform(currentContent)); - } - - return this; - } - - /// - /// Prevents the files from being restored to their original content upon disposal. - /// - public void CancelRestore() => - _cancelled = true; -} diff --git a/DecSm.Atom/Process/ProcessRunOptions.cs b/DecSm.Atom/Process/ProcessRunOptions.cs deleted file mode 100644 index 928e479e..00000000 --- a/DecSm.Atom/Process/ProcessRunOptions.cs +++ /dev/null @@ -1,101 +0,0 @@ -namespace DecSm.Atom.Process; - -/// -/// Configuration options for executing external processes through the service. -/// -/// -/// This record provides a comprehensive set of options for controlling how external processes are executed, -/// including logging behavior, error handling, and execution context. It supports both string-based and -/// array-based argument specification. -/// -/// Default values are configured for common build automation scenarios: -/// -/// Process invocation logged at . -/// Standard output logged at . -/// Error output logged at . -/// Failed processes throw by default. -/// -/// -/// -/// The name or path of the executable to run (e.g., "dotnet", "git"). -/// The command-line arguments to pass to the executable as a single string. -[PublicAPI] -public sealed record ProcessRunOptions(string Name, string Args) -{ - /// - /// Initializes a new instance of with the specified executable name and an array of - /// arguments. - /// - /// The name or path of the executable to run. - /// An array of command-line arguments, which will be joined into a single string with spaces. - public ProcessRunOptions(string Name, string[] Args) : this(Name, - string.Join(" ", Args.Where(a => !string.IsNullOrWhiteSpace(a)))) { } - - /// - /// Gets or sets the working directory for the process execution. - /// - /// - /// If null, the current application's working directory is used. Relative paths are resolved - /// relative to the current working directory. The directory must exist when the process starts. - /// - public string? WorkingDirectory { get; init; } - - /// - /// Gets or sets the log level for messages indicating the process invocation. - /// - /// - /// Defaults to . - /// - public LogLevel InvocationLogLevel { get; init; } = LogLevel.Information; - - /// - /// Gets or sets the log level for standard output messages from the executed process. - /// - /// - /// Defaults to . - /// - public LogLevel OutputLogLevel { get; init; } = LogLevel.Debug; - - /// - /// Gets or sets the log level for standard error messages from the executed process. - /// - /// - /// Defaults to . - /// - public LogLevel ErrorLogLevel { get; init; } = LogLevel.Warning; - - /// - /// Gets or sets a value indicating whether the process execution should tolerate non-zero exit codes. - /// - /// - /// If false (default), a non-zero exit code will cause a to be thrown. - /// If true, the process completes normally, and the caller must check . - /// - public bool AllowFailedResult { get; init; } - - /// - /// Gets or sets a dictionary of environment variables to be used by the process. - /// - /// - /// Keys are variable names, and values are their corresponding values. A null value removes the variable. - /// If not set, the process inherits the current environment's variables. - /// - public Dictionary EnvironmentVariables { get; init; } = []; - - /// - /// An optional per-line transformation applied to the standard output (stdout) of the executed process. - /// - /// - /// The delegate receives a raw line of text. Returning null suppresses the line from being logged or captured. - /// This is useful for filtering, redacting, or normalizing output. - /// - public Func? TransformOutput { get; init; } - - /// - /// An optional per-line transformation applied to the standard error (stderr) of the executed process. - /// - /// - /// Similar to , this can be used to filter, redact, or normalize error messages. - /// - public Func? TransformError { get; init; } -} diff --git a/DecSm.Atom/Process/ProcessRunResult.cs b/DecSm.Atom/Process/ProcessRunResult.cs deleted file mode 100644 index 2dba52de..00000000 --- a/DecSm.Atom/Process/ProcessRunResult.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace DecSm.Atom.Process; - -/// -/// Represents the result of an external process execution, including its exit code and captured output. -/// -/// The options that were used to run the process. -/// The exit code returned by the process. -/// The standard output (stdout) captured from the process. -/// The standard error (stderr) captured from the process. -[PublicAPI] -public sealed record ProcessRunResult(ProcessRunOptions RunOptions, int ExitCode, string Output, string Error); diff --git a/DecSm.Atom/Process/ProcessRunner.cs b/DecSm.Atom/Process/ProcessRunner.cs deleted file mode 100644 index 2e911dfd..00000000 --- a/DecSm.Atom/Process/ProcessRunner.cs +++ /dev/null @@ -1,297 +0,0 @@ -namespace DecSm.Atom.Process; - -/// -/// Defines a service for executing external processes with comprehensive logging and error handling. -/// -public interface IProcessRunner -{ - /// - /// Executes an external process synchronously. - /// - /// The configuration options for the process execution. - /// A containing the exit code and captured output. - /// - /// Thrown if the process returns a non-zero exit code and is - /// false. - /// - ProcessRunResult Run(ProcessRunOptions options); - - /// - /// Executes an external process asynchronously. - /// - /// The configuration options for the process execution. - /// A token to cancel the process. - /// A task that resolves to a containing the exit code and captured output. - /// - /// Thrown if the process returns a non-zero exit code and is - /// false. - /// - /// Thrown if the operation is canceled. - Task RunAsync(ProcessRunOptions options, CancellationToken cancellationToken = default); -} - -/// -/// Provides a standardized service for executing external processes with comprehensive logging, error handling, and -/// result capture. -/// -/// -/// This class wraps to provide a more robust and configurable execution -/// model -/// for build automation, including real-time stream capture, flexible error handling, and cancellation support. -/// -/// The logger for capturing process execution information. -[PublicAPI] -public sealed class ProcessRunner(ILogger logger) : IProcessRunner -{ - /// - public ProcessRunResult Run(ProcessRunOptions options) - { - switch (options) - { - case { WorkingDirectory.Length: > 0, EnvironmentVariables.Count: > 0 }: - logger.Log(options.InvocationLogLevel, - "Run: {Name} {Args} in {WorkingDirectory} with env {EnvironmentVariables}", - options.Name, - options.Args, - options.WorkingDirectory, - string.Join(", ", options.EnvironmentVariables.Select(kv => $"{kv.Key}={kv.Value}"))); - - break; - case { WorkingDirectory.Length: > 0 }: - logger.Log(options.InvocationLogLevel, - "Run: {Name} {Args} in {WorkingDirectory}", - options.Name, - options.Args, - options.WorkingDirectory); - - break; - case { EnvironmentVariables.Count: > 0 }: - logger.Log(options.InvocationLogLevel, - "Run: {Name} {Args} with env {EnvironmentVariables}", - options.Name, - options.Args, - string.Join(", ", options.EnvironmentVariables.Select(kv => $"{kv.Key}={kv.Value}"))); - - break; - default: - logger.Log(options.InvocationLogLevel, "Run: {Name} {Args}", options.Name, options.Args); - - break; - } - - using var process = new System.Diagnostics.Process(); - - process.StartInfo = new() - { - FileName = options.Name, - Arguments = options.Args, - WorkingDirectory = options.WorkingDirectory, - RedirectStandardOutput = true, - RedirectStandardError = true, - }; - - foreach (var environmentVariable in options.EnvironmentVariables) - process.StartInfo.Environment[environmentVariable.Key] = environmentVariable.Value; - - var outputBuilder = new StringBuilder(); - var errorBuilder = new StringBuilder(); - - process.OutputDataReceived += OnProcessOnOutputDataReceived; - process.ErrorDataReceived += OnProcessOnErrorDataReceived; - - try - { - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); - } - finally - { - process.OutputDataReceived -= OnProcessOnOutputDataReceived; - process.ErrorDataReceived -= OnProcessOnErrorDataReceived; - } - - var output = outputBuilder.ToString(); - var error = errorBuilder.ToString(); - - var result = new ProcessRunResult(options, process.ExitCode, output, error); - - if (result.ExitCode is 0) - return result; - - if (options.OutputLogLevel < LogLevel.Information) - logger.LogInformation("{Output}", output); - - if (options.ErrorLogLevel < LogLevel.Information) - logger.LogWarning("{Error}", error); - - if (options.AllowFailedResult) - { - logger.Log(options.InvocationLogLevel, "Process finished with exit code {ExitCode}", result.ExitCode); - - return result; - } - - throw new StepFailedException($"Process {options.Name} {options.Args} failed with exit code {result.ExitCode}"); - - void OnProcessOnErrorDataReceived(object _, DataReceivedEventArgs e) - { - if (e.Data is null) - return; - - var text = options.TransformError is not null - ? options.TransformError(e.Data) - : e.Data; - - if (text is null) - return; - - errorBuilder.AppendLine(text); - logger.Log(options.ErrorLogLevel, "{Error}", text); - } - - void OnProcessOnOutputDataReceived(object _, DataReceivedEventArgs e) - { - if (e.Data is null) - return; - - var text = options.TransformOutput is not null - ? options.TransformOutput(e.Data) - : e.Data; - - if (text is null) - return; - - outputBuilder.AppendLine(text); - logger.Log(options.OutputLogLevel, "{Output}", text); - } - } - - /// - public async Task RunAsync( - ProcessRunOptions options, - CancellationToken cancellationToken = default) - { - switch (options) - { - case { WorkingDirectory.Length: > 0, EnvironmentVariables.Count: > 0 }: - logger.Log(options.InvocationLogLevel, - "Run: {Name} {Args} in {WorkingDirectory} with env {EnvironmentVariables}", - options.Name, - options.Args, - options.WorkingDirectory, - string.Join(", ", options.EnvironmentVariables.Select(kv => $"{kv.Key}={kv.Value}"))); - - break; - case { WorkingDirectory.Length: > 0 }: - logger.Log(options.InvocationLogLevel, - "Run: {Name} {Args} in {WorkingDirectory}", - options.Name, - options.Args, - options.WorkingDirectory); - - break; - case { EnvironmentVariables.Count: > 0 }: - logger.Log(options.InvocationLogLevel, - "Run: {Name} {Args} with env {EnvironmentVariables}", - options.Name, - options.Args, - string.Join(", ", options.EnvironmentVariables.Select(kv => $"{kv.Key}={kv.Value}"))); - - break; - default: - logger.Log(options.InvocationLogLevel, "Run: {Name} {Args}", options.Name, options.Args); - - break; - } - - using var process = new System.Diagnostics.Process(); - - process.StartInfo = new() - { - FileName = options.Name, - Arguments = options.Args, - WorkingDirectory = options.WorkingDirectory, - RedirectStandardOutput = true, - RedirectStandardError = true, - }; - - foreach (var environmentVariable in options.EnvironmentVariables) - process.StartInfo.Environment[environmentVariable.Key] = environmentVariable.Value; - - var outputBuilder = new StringBuilder(); - var errorBuilder = new StringBuilder(); - - process.OutputDataReceived += OnProcessOnOutputDataReceived; - process.ErrorDataReceived += OnProcessOnErrorDataReceived; - - try - { - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - await process.WaitForExitAsync(cancellationToken); - } - finally - { - process.OutputDataReceived -= OnProcessOnOutputDataReceived; - process.ErrorDataReceived -= OnProcessOnErrorDataReceived; - } - - var output = outputBuilder.ToString(); - var error = errorBuilder.ToString(); - - var result = new ProcessRunResult(options, process.ExitCode, output, error); - - if (result.ExitCode is 0) - return result; - - if (options.OutputLogLevel < LogLevel.Information) - logger.LogInformation("{Output}", output); - - if (options.ErrorLogLevel < LogLevel.Information) - logger.LogWarning("{Error}", error); - - if (options.AllowFailedResult) - { - logger.Log(options.InvocationLogLevel, "Process finished with exit code {ExitCode}", result.ExitCode); - - return result; - } - - throw new StepFailedException($"Process {options.Name} {options.Args} failed with exit code {result.ExitCode}"); - - void OnProcessOnErrorDataReceived(object _, DataReceivedEventArgs e) - { - if (e.Data is null) - return; - - var text = options.TransformError is not null - ? options.TransformError(e.Data) - : e.Data; - - if (text is null) - return; - - errorBuilder.AppendLine(text); - logger.Log(options.ErrorLogLevel, "{Error}", text); - } - - void OnProcessOnOutputDataReceived(object _, DataReceivedEventArgs e) - { - if (e.Data is null) - return; - - var text = options.TransformOutput is not null - ? options.TransformOutput(e.Data) - : e.Data; - - if (text is null) - return; - - outputBuilder.AppendLine(text); - logger.Log(options.OutputLogLevel, "{Output}", text); - } - } -} diff --git a/DecSm.Atom/SemVer.cs b/DecSm.Atom/SemVer.cs deleted file mode 100644 index ca85d358..00000000 --- a/DecSm.Atom/SemVer.cs +++ /dev/null @@ -1,507 +0,0 @@ -namespace DecSm.Atom; - -/// -/// Represents a semantic version following the Semantic Versioning 2.0.0 specification. -/// Supports parsing, comparison, and conversion operations for version strings in the format -/// MAJOR.MINOR.PATCH[-PRERELEASE][+METADATA]. -/// -/// -/// This class implements semantic version comparison where: -/// -/// Release versions (without pre-release) are greater than pre-release versions -/// Pre-release versions are compared lexicographically by dot-separated identifiers -/// Numeric identifiers in pre-release are compared numerically -/// Metadata is used only as a final tiebreaker when all other components are equal -/// -/// Supports implicit conversion to/from string and JSON serialization. -/// -/// -/// -/// var version1 = SemVer.Parse("1.2.3-alpha.1+build.123"); -/// var version2 = new SemVer("2.0.0"); -/// bool isNewer = version2 > version1; // true -/// -/// -[PublicAPI] -public sealed partial class SemVer() - : ISpanParsable, IComparable, IComparisonOperators -{ - /// - /// Initializes a new instance of the class with the specified version components. - /// This constructor is used for JSON deserialization. - /// - /// The major version number. - /// The minor version number. - /// The patch version number. - /// The pre-release version identifier, or null if not a pre-release. - /// The build metadata, or null if no metadata is present. - [JsonConstructor] - private SemVer(int major, int minor, int patch, string? preRelease, string? metadata) : this() - { - Major = major; - Minor = minor; - Patch = patch; - PreRelease = preRelease; - Metadata = metadata; - } - - /// - /// Gets the major version number. - /// - /// The major version number, indicating incompatible API changes. - public int Major { get; private init; } - - /// - /// Gets the minor version number. - /// - /// The minor version number, indicating backwards-compatible functionality additions. - public int Minor { get; private init; } - - /// - /// Gets the patch version number. - /// - /// The patch version number, indicating backwards-compatible bug fixes. - public int Patch { get; private init; } - - /// - /// Gets the version prefix in the format "MAJOR.MINOR.PATCH". - /// - /// A string representing the core version without pre-release or metadata components. - [JsonIgnore] - public string Prefix => $"{Major}.{Minor}.{Patch}"; - - /// - /// Gets the pre-release version identifier. - /// - /// The pre-release identifier string, or null if this is a release version. - /// - /// Pre-release identifiers are dot-separated and may contain alphanumeric characters and hyphens. - /// - public string? PreRelease { get; private init; } - - /// - /// Gets a value indicating whether this version is a pre-release version. - /// - /// true if this version has a pre-release identifier; otherwise, false. - [JsonIgnore] - public bool IsPreRelease => PreRelease != null; - - /// - /// Gets the build number extracted from the pre-release identifier. - /// - /// The numeric build number if exactly one number is found in the pre-release; otherwise, 0. - /// - /// This property uses to parse the pre-release string. - /// Only returns a non-zero value if exactly one numeric sequence is present. - /// - [JsonIgnore] - public int BuildNumberFromPreRelease => ExtractBuildNumber(PreRelease); - - /// - /// Gets the build metadata. - /// - /// The build metadata string, or null if no metadata is present. - /// - /// Metadata is ignored when determining version precedence but is used as a tiebreaker in comparisons. - /// - public string? Metadata { get; private init; } - - /// - /// Gets the build number extracted from the metadata. - /// - /// The numeric build number if exactly one number is found in the metadata; otherwise, 0. - /// - /// This property uses to parse the metadata string. - /// Only returns a non-zero value if exactly one numeric sequence is present. - /// - [JsonIgnore] - public int BuildNumberFromMetadata => ExtractBuildNumber(Metadata); - - /// - /// Gets a semantic version representing version 1.0.0. - /// - /// A instance with Major set to 1, Minor set to 0, and Patch set to 0. - public static SemVer One { get; } = new() - { - Major = 1, - Minor = 0, - Patch = 0, - }; - - /// - /// Compares the current instance with another and returns an integer that indicates - /// whether the current instance precedes, follows, or occurs in the same position in the sort order. - /// - /// The to compare with this instance. - /// - /// A value that indicates the relative order of the objects being compared: - /// Less than zero if this instance precedes ; - /// Zero if they are equal; - /// Greater than zero if this instance follows . - /// - /// - /// Comparison follows Semantic Versioning precedence rules: - /// - /// Compare major, minor, and patch versions numerically - /// Pre-release versions have lower precedence than normal versions - /// Pre-release identifiers are compared lexicographically, with numeric identifiers compared numerically - /// Metadata is used only as a final tiebreaker - /// - /// - public int CompareTo(SemVer? other) - { - if (other is null) - return 1; - - var majorComparison = Major.CompareTo(other.Major); - - if (majorComparison != 0) - return majorComparison; - - var minorComparison = Minor.CompareTo(other.Minor); - - if (minorComparison != 0) - return minorComparison; - - var patchComparison = Patch.CompareTo(other.Patch); - - if (patchComparison != 0) - return patchComparison; - - switch (PreRelease, other.PreRelease) - { - case (null, not null): - return 1; - case (not null, null): - return -1; - case (null, null): - return string.CompareOrdinal(Metadata, other.Metadata); - } - - var preReleaseParts = PreRelease.Split('.'); - var otherPreReleaseParts = other.PreRelease.Split('.'); - - for (var i = 0; i < Math.Min(preReleaseParts.Length, otherPreReleaseParts.Length); i++) - if (int.TryParse(preReleaseParts[i], out var preReleasePart) && - int.TryParse(otherPreReleaseParts[i], out var otherPreReleasePart)) - { - var preReleasePartComparison = preReleasePart.CompareTo(otherPreReleasePart); - - if (preReleasePartComparison != 0) - return preReleasePartComparison; - } - else - { - var preReleasePartComparison = string.CompareOrdinal(preReleaseParts[i], otherPreReleaseParts[i]); - - if (preReleasePartComparison != 0) - return preReleasePartComparison; - } - - var preReleaseLengthComparison = preReleaseParts.Length.CompareTo(otherPreReleaseParts.Length); - - return preReleaseLengthComparison != 0 - ? preReleaseLengthComparison - : string.CompareOrdinal(Metadata, other.Metadata); - } - - public static bool operator >(SemVer left, SemVer right) => - left.CompareTo(right) > 0; - - public static bool operator >=(SemVer left, SemVer right) => - left.CompareTo(right) >= 0; - - public static bool operator <(SemVer left, SemVer right) => - left.CompareTo(right) < 0; - - public static bool operator <=(SemVer left, SemVer right) => - left.CompareTo(right) <= 0; - - public static bool operator ==(SemVer? left, SemVer? right) => - (left is null && right is null) || left?.Equals(right) == true; - - public static bool operator !=(SemVer? left, SemVer? right) => - !(left == right); - - public static SemVer Parse(string s, IFormatProvider? provider) - { - var match = SemVerRegex() - .Match(s); - - // ReSharper disable once LocalizableElement - if (!match.Success) - throw new ArgumentException($"Invalid version string '{s}'.", nameof(s)); - - return new() - { - Major = int.Parse(match.Groups[1].Value), - Minor = int.Parse(match.Groups[2].Value), - Patch = int.Parse(match.Groups[3].Value), - PreRelease = string.IsNullOrWhiteSpace(match.Groups[4].Value) - ? null - : match.Groups[4].Value, - Metadata = string.IsNullOrWhiteSpace(match.Groups[5].Value) - ? null - : match.Groups[5].Value, - }; - } - - public static bool TryParse( - [NotNullWhen(true)] string? s, - IFormatProvider? provider, - [MaybeNullWhen(false)] out SemVer result) - { - if (s is null) - { - result = null; - - return false; - } - - var match = SemVerRegex() - .Match(s); - - if (!match.Success || - !int.TryParse(match.Groups[1].Value, out var major) || - major < 0 || - !int.TryParse(match.Groups[2].Value, out var minor) || - minor < 0 || - !int.TryParse(match.Groups[3].Value, out var patch) || - patch < 0) - { - result = null; - - return false; - } - - var preRelease = match.Groups[4].Value; - var metadata = match.Groups[5].Value; - - result = new() - { - Major = major, - Minor = minor, - Patch = patch, - PreRelease = string.IsNullOrWhiteSpace(preRelease) - ? null - : preRelease, - Metadata = string.IsNullOrWhiteSpace(metadata) - ? null - : metadata, - }; - - return true; - } - - public static SemVer Parse(ReadOnlySpan s, IFormatProvider? provider) => - Parse(s.ToString(), provider); - - public static bool TryParse( - ReadOnlySpan s, - IFormatProvider? provider, - [MaybeNullWhen(false)] out SemVer result) => - TryParse(s.ToString(), provider, out result); - - /// - /// Determines whether the current version is between two specified versions (exclusive). - /// - /// The first boundary version. - /// The second boundary version. - /// true if this version is between the two bounds; otherwise, false. - /// - /// The method handles bounds in any order. If both bounds are equal, returns true only if this version equals the - /// bounds. - /// - public bool IsBetween(SemVer firstBound, SemVer secondBound) => - firstBound == secondBound - ? Equals(firstBound) - : firstBound < secondBound - ? firstBound < this && this < secondBound - : secondBound < this && this < firstBound; - - /// - /// Converts this semantic version to a instance. - /// - /// If true, throws an exception when pre-release data is present. - /// If true, throws an exception when metadata is present. - /// A with Major, Minor, and Build (from Patch) components. - /// - /// Thrown when is true and pre-release data is present, - /// or when is true and metadata is present. - /// - /// - /// Maps SemVer Major.Minor.Patch to System.Version Major.Minor.Build. - /// Pre-release and metadata information is lost in the conversion. - /// - public Version ToSystemVersion(bool throwIfContainsPreRelease = false, bool throwIfContainsMetadata = false) => - throwIfContainsPreRelease && PreRelease is not null - ? throw new ArgumentException( - "The SemVer contains a pre-release tag, which is not supported by System.Version.") - : throwIfContainsMetadata && Metadata is not null - ? throw new ArgumentException("The SemVer contains metadata, which is not supported by System.Version.") - : new(Major, Minor, Patch); - - /// - /// Creates a instance from a . - /// - /// The System.Version to convert. - /// If true, throws an exception when the version has a non-zero revision. - /// A with Major, Minor, and Patch (from Build) components. - /// - /// Thrown when is true and the version has a non-zero revision. - /// - /// - /// Maps System.Version Major.Minor.Build to SemVer Major.Minor.Patch. - /// The revision component is ignored unless is true. - /// - public static SemVer FromSystemVersion(Version version, bool throwIfContainsRevision = false) => - throwIfContainsRevision && version.Revision > 0 - ? throw new ArgumentException("The version contains a revision number, which is not supported by SemVer.") - : new() - { - Major = version.Major, - Minor = version.Minor, - Patch = version.Build, - }; - - /// - /// Extracts a single numeric value from the specified input string. - /// - /// The string to extract a number from. - /// - /// The numeric value if exactly one number is found; otherwise, 0. - /// Returns 0 if the input is null, empty, whitespace, or contains zero or multiple numbers. - /// - /// - /// This method is used by and - /// to extract build numbers from version components. - /// - /// - /// - /// ExtractBuildNumber("alpha.123") // returns 123 - /// ExtractBuildNumber("beta") // returns 0 - /// ExtractBuildNumber("1.2.3") // returns 0 (multiple numbers) - /// - /// - public static int ExtractBuildNumber(string? input) - { - if (string.IsNullOrWhiteSpace(input)) - return 0; - - var matches = NumberRegex() - .Matches(input); - - return matches.Count is 1 - ? int.Parse(matches[0].Value) - : 0; - } - - /// - /// Parses a string representation of a semantic version. - /// - /// The string to parse. - /// A equivalent to the version contained in . - /// Thrown when is not a valid semantic version string. - /// - /// Accepts version strings in the format: MAJOR.MINOR.PATCH[-PRERELEASE][+METADATA] - /// where MAJOR, MINOR, and PATCH are non-negative integers. - /// - public static SemVer Parse(string s) => - Parse(s, null); - - /// - /// Tries to parse a string representation of a semantic version using the invariant culture. - /// - /// The string to parse. - /// - /// When this method returns, contains the equivalent to the version contained in - /// , - /// if the conversion succeeded, or null if the conversion failed. - /// - /// true if was converted successfully; otherwise, false. - public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out SemVer result) => - TryParse(s, null, out result); - - /// - /// Tries to parse a span of characters representing a semantic version using the invariant culture. - /// - /// The span of characters to parse. - /// - /// When this method returns, contains the equivalent to the version contained in - /// , - /// if the conversion succeeded, or null if the conversion failed. - /// - /// true if was converted successfully; otherwise, false. - public static bool TryParse(ReadOnlySpan s, [MaybeNullWhen(false)] out SemVer result) => - TryParse(s.ToString(), out result); - - /// - /// Determines whether the specified is equal to the current instance. - /// - /// The to compare with the current instance. - /// true if the specified is equal to the current instance; otherwise, false. - public bool Equals(SemVer? other) => - CompareTo(other) == 0; - - /// - /// Determines whether the specified object is equal to the current instance. - /// - /// The object to compare with the current instance. - /// true if the specified object is equal to the current instance; otherwise, false. - public override bool Equals(object? obj) => - obj is SemVer semVer && Equals(semVer); - - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. - public override int GetHashCode() => - HashCode.Combine(Major, Minor, Patch, PreRelease, Metadata); - - /// - /// Converts the string representation of a semantic version to its equivalent. - /// - /// The string representation of the semantic version. - /// A object equivalent to the version contained in . - /// Thrown when is not a valid semantic version string. - public static implicit operator SemVer(string s) => - Parse(s); - - /// - /// Converts a to its string representation. - /// - /// The to convert. - /// The string representation of the semantic version. - public static implicit operator string(SemVer semVer) => - semVer.ToString(); - - /// - /// Returns the string representation of this semantic version. - /// - /// - /// A string in the format MAJOR.MINOR.PATCH[-PRERELEASE][+METADATA], - /// where the pre-release and metadata components are included only if present. - /// - /// - /// - /// new SemVer { Major = 1, Minor = 2, Patch = 3 }.ToString() // "1.2.3" - /// new SemVer { Major = 1, Minor = 2, Patch = 3, PreRelease = "alpha" }.ToString() // "1.2.3-alpha" - /// new SemVer { Major = 1, Minor = 2, Patch = 3, Metadata = "build.1" }.ToString() // "1.2.3+build.1" - /// - /// - public override string ToString() => - (PreRelease, Metadata) switch - { - (not null, not null) => $"{Major}.{Minor}.{Patch}-{PreRelease}+{Metadata}", - (not null, null) => $"{Major}.{Minor}.{Patch}-{PreRelease}", - (null, not null) => $"{Major}.{Minor}.{Patch}+{Metadata}", - _ => $"{Major}.{Minor}.{Patch}", - }; - - [GeneratedRegex( - @"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$")] - private static partial Regex SemVerRegex(); - - [GeneratedRegex(@"\d+")] - private static partial Regex NumberRegex(); -} diff --git a/DecSm.Atom/Util/ServiceStaticAccessor.cs b/DecSm.Atom/Util/ServiceStaticAccessor.cs deleted file mode 100644 index 9cd6bc21..00000000 --- a/DecSm.Atom/Util/ServiceStaticAccessor.cs +++ /dev/null @@ -1,82 +0,0 @@ -namespace DecSm.Atom.Util; - -/// -/// Provides static access to a service instance registered in the dependency injection container. -/// -/// The type of the service to access. -/// -/// This class implements the service locator pattern, allowing access to services without explicit -/// constructor injection. It should be used sparingly, as it can make code harder to test. -/// The property is populated by the extension methods in -/// . -/// -/// Warning: The service is only available after it has been resolved from the DI container. -/// -/// -/// -/// -/// // 1. Register the service -/// services.AddSingletonWithStaticAccessor<IMyService, MyService>(); -/// // 2. Access the service statically from anywhere -/// ServiceStaticAccessor<IMyService>.Service?.DoSomething(); -/// -/// -/// -public static class ServiceStaticAccessor - where T : notnull -{ - /// - /// Gets or sets the service instance. This property is populated by the DI container. - /// - public static T? Service { get; set; } -} - -/// -/// Provides extension methods for registering services with static accessor functionality. -/// -internal static class ServiceAccessorExtensions -{ - extension(IServiceCollection services) - { - /// - /// Registers a singleton service that can be accessed statically via . - /// - /// The implementation type of the service. - public void AddSingletonWithStaticAccessor< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>() - where TImplementation : class => - services - .AddKeyedSingleton("StaticAccess") - .AddSingleton(x => - ServiceStaticAccessor.Service = - x.GetRequiredKeyedService("StaticAccess")); - - /// - /// Registers a singleton service with a separate interface and implementation that can be accessed statically. - /// - /// The service interface type. - /// The service implementation type. - public void AddSingletonWithStaticAccessor() - where TService : class - where TImplementation : class, TService => - services - .AddKeyedSingleton("StaticAccess") - .AddSingleton(x => - ServiceStaticAccessor.Service = x.GetRequiredKeyedService("StaticAccess")); - - /// - /// Registers a singleton service using a factory function that can be accessed statically. - /// - /// The type of the service to register. - /// A factory function that creates the service instance. - /// The for method chaining. - public IServiceCollection AddSingletonWithStaticAccessor( - Func implementationFactory) - where TService : class => - services - .AddKeyedSingleton("StaticAccess", implementationFactory) - .AddSingleton(x => - ServiceStaticAccessor.Service = x.GetRequiredKeyedService("StaticAccess")); - } -} diff --git a/DecSm.Atom/Workflows/Definition/Options/IWorkflowOption.cs b/DecSm.Atom/Workflows/Definition/Options/IWorkflowOption.cs deleted file mode 100644 index 4f706e5d..00000000 --- a/DecSm.Atom/Workflows/Definition/Options/IWorkflowOption.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace DecSm.Atom.Workflows.Definition.Options; - -/// -/// Represents a workflow option that can be applied to control workflow behavior and configuration. -/// -/// -/// This interface is the base for all workflow configuration options, including custom steps and parameter injections. -/// It provides built-in support for option merging and deduplication. -/// -[PublicAPI] -public interface IWorkflowOption -{ - /// - /// Gets a value indicating whether multiple instances of this option are allowed in a workflow. - /// - /// - /// If false (default), only the last occurrence of this option type is retained during merging. - /// If true, all instances are preserved. - /// - bool AllowMultiple => false; - - /// - /// Retrieves and merges all workflow options applicable to the currently running workflows. - /// - /// The build definition containing global options and active workflows. - /// A merged collection of workflow options from global and active workflow configurations. - static IEnumerable GetOptionsForCurrentTarget(IBuildDefinition buildDefinition) => - Merge(buildDefinition.GlobalWorkflowOptions.Concat(buildDefinition - .Workflows - .Where(workflow => workflow.WorkflowTypes.Any(workflowType => workflowType.IsRunning)) - .SelectMany(workflow => workflow.Options))); - - /// - /// Merges a collection of workflow options, handling deduplication based on the property. - /// - /// The type of workflow option being merged. - /// The collection of workflow options to merge. - /// A collection of merged options with duplicates resolved. - static IEnumerable Merge(IEnumerable entries) - where T : IWorkflowOption => - entries - .GroupBy(x => x.GetType()) - .SelectMany(x => x - .First() - .MergeWith(x.Skip(1))); - - /// - /// Merges the current option with additional instances of the same type, respecting the - /// configuration. - /// - /// The type of workflow option being merged. - /// Additional instances of the same option type to merge. - /// - /// A collection containing either all instances (if is true) - /// or only the last instance (if false). - /// - private IEnumerable MergeWith(IEnumerable entries) - where T : IWorkflowOption => - entries.ToArray() is { Length: > 0 } entriesArray - ? AllowMultiple - ? entriesArray.Prepend((T)this) - : [entriesArray[^1]] - : [(T)this]; -} diff --git a/DecSm.Atom/Workflows/Definition/Options/ToggleWorkflowOption.cs b/DecSm.Atom/Workflows/Definition/Options/ToggleWorkflowOption.cs deleted file mode 100644 index eb2f24ed..00000000 --- a/DecSm.Atom/Workflows/Definition/Options/ToggleWorkflowOption.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace DecSm.Atom.Workflows.Definition.Options; - -/// -/// An abstract base record for creating workflow options that act as boolean toggles. -/// -/// The concrete type implementing this toggle option. -/// -/// This class provides a foundation for creating boolean workflow options with predefined -/// and states, along with a helper method for checking the enabled state. -/// -/// -/// -/// public sealed record UseCustomFeature : ToggleWorkflowOption<UseCustomFeature>; -/// // Usage: -/// var options = new List<IWorkflowOption> { UseCustomFeature.Enabled }; -/// bool isEnabled = UseCustomFeature.IsEnabled(options); // true -/// -/// -[PublicAPI] -public abstract record ToggleWorkflowOption : WorkflowOption - where TSelf : WorkflowOption, new() -{ - /// - /// Gets a predefined instance representing the enabled state (true). - /// - public static readonly TSelf Enabled = new() - { - Value = true, - }; - - /// - /// Gets a predefined instance representing the disabled state (false). - /// - public static readonly TSelf Disabled = new() - { - Value = false, - }; - - /// - /// Determines whether this toggle option is enabled within a collection of workflow options. - /// - /// The collection of workflow options to check. - /// true if an enabled instance of this option exists in the collection; otherwise, false. -#pragma warning disable RCS1158 - public static bool IsEnabled(IEnumerable options) => - options - .OfType() - .Any(x => x.Value); -#pragma warning restore RCS1158 -} diff --git a/DecSm.Atom/Workflows/Definition/Options/WorkflowOption.cs b/DecSm.Atom/Workflows/Definition/Options/WorkflowOption.cs deleted file mode 100644 index a61baf67..00000000 --- a/DecSm.Atom/Workflows/Definition/Options/WorkflowOption.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace DecSm.Atom.Workflows.Definition.Options; - -/// -/// An abstract base record for creating strongly-typed, data-carrying workflow options. -/// -/// The type of data this option carries. -/// The concrete type implementing this option, used for fluent APIs. -/// -/// This class serves as the foundation for all data-carrying workflow options, implementing -/// -/// while providing type-safe data handling. The self-referencing generic pattern ensures that methods on derived types -/// return the correct concrete type. -/// -/// -/// -/// public sealed record BuildConfiguration : WorkflowOption<string, BuildConfiguration>; -/// // Usage: -/// var config = BuildConfiguration.Create("Release"); -/// -/// -[PublicAPI] -public abstract record WorkflowOption : IWorkflowOption - where TSelf : WorkflowOption, new() -{ - /// - /// Gets the data value carried by this workflow option. - /// - public virtual TData? Value { get; init; } - - /// - /// Gets a value indicating whether multiple instances of this option are allowed in a workflow. - /// Defaults to false. - /// - public virtual bool AllowMultiple => false; - - /// - /// Creates a new instance of the workflow option with the specified value. - /// - /// The value to assign to the option. - /// A new instance of the workflow option. - public static TSelf Create(TData value) => - new() - { - Value = value, - }; -} diff --git a/DecSm.Atom/Workflows/Definition/Options/WorkflowParamInjection.cs b/DecSm.Atom/Workflows/Definition/Options/WorkflowParamInjection.cs deleted file mode 100644 index 30234b1f..00000000 --- a/DecSm.Atom/Workflows/Definition/Options/WorkflowParamInjection.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace DecSm.Atom.Workflows.Definition.Options; - -/// -/// Represents a workflow option that injects a parameter value into the workflow execution context. -/// -/// The name of the parameter to inject. -/// The value to inject for the specified parameter. -/// -/// This allows workflows to set specific parameter values that take precedence over other sources -/// like command-line arguments or environment variables. -/// -/// -/// -/// // Inject a dry-run parameter -/// var dryRunInjection = new WorkflowParamInjection("NugetDryRun", "true"); -/// // Add to workflow configuration -/// var workflowDefinition = new WorkflowDefinition().WithAddedOptions(dryRunInjection); -/// -/// -[PublicAPI] -public sealed record WorkflowParamInjection(string Name, string Value) : IWorkflowOption -{ - /// - /// Gets a value indicating that multiple instances of this option are allowed. - /// - public bool AllowMultiple => true; - - /// - /// Merges multiple instances, ensuring that for each parameter name, - /// only the last injected value is retained. - /// - /// The type of workflow option being merged. - /// The collection of workflow options to merge. - /// A collection of merged options with the latest value for each parameter. - public static IEnumerable MergeWith(IEnumerable entries) - where T : IWorkflowOption => - entries - .OfType() - .GroupBy(x => x.Name) - .Select(x => x.Last()) - .Cast(); -} diff --git a/DecSm.Atom/Workflows/Definition/Triggers/ScheduleTrigger.cs b/DecSm.Atom/Workflows/Definition/Triggers/ScheduleTrigger.cs deleted file mode 100644 index 4c0a9a45..00000000 --- a/DecSm.Atom/Workflows/Definition/Triggers/ScheduleTrigger.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DecSm.Atom.Workflows.Definition.Triggers; - -/// -/// Represents a workflow trigger that fires based on a defined CRON schedule. -/// -/// -/// A string representing the CRON schedule for the trigger (e.g., "0 0 * * *" for daily midnight UTC). -/// -/// -/// This trigger is used to automatically run a workflow at specified times or intervals. -/// The validity and interpretation of the CRON expression are subject to the specific CI/CD platform's parser. -/// -[PublicAPI] -public sealed record GithubScheduleTrigger(string CronExpression) : IWorkflowTrigger; diff --git a/DecSm.Atom/Workflows/IWorkflowOptionProvider.cs b/DecSm.Atom/Workflows/IWorkflowOptionProvider.cs deleted file mode 100644 index 7b594208..00000000 --- a/DecSm.Atom/Workflows/IWorkflowOptionProvider.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace DecSm.Atom.Workflows; - -/// -/// Defines a contract for providing workflow options. -/// -/// -/// This interface enables classes to expose a collection of workflow options that can be consumed by -/// workflow engines or configuration systems. Implementations should return a stable, immutable collection. -/// -[PublicAPI] -public interface IWorkflowOptionProvider -{ - /// - /// Gets a read-only collection of workflow options provided by this instance. - /// - /// - /// The collection should never be null, but may be empty. It should be treated as immutable by consumers. - /// - IReadOnlyList WorkflowOptions { get; } -} diff --git a/DecSm.Atom/Workflows/Model/WorkflowJobModel.cs b/DecSm.Atom/Workflows/Model/WorkflowJobModel.cs deleted file mode 100644 index bb4a5c2a..00000000 --- a/DecSm.Atom/Workflows/Model/WorkflowJobModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace DecSm.Atom.Workflows.Model; - -/// -/// Represents a job within a workflow, including its name, steps, dependencies, and configuration options. -/// -/// The name of the job. -/// The sequence of steps to be executed in the job. -[PublicAPI] -public sealed record WorkflowJobModel(string Name, IReadOnlyList Steps) -{ - /// - /// Gets the names of other jobs that must be completed before this job can start. - /// - public required IReadOnlyList JobDependencies { get; init; } - - /// - /// Gets the options that configure this job's behavior. - /// - public required IReadOnlyList Options { get; init; } - - /// - /// Gets the matrix dimensions for running this job in multiple configurations. - /// - public required IReadOnlyList MatrixDimensions { get; init; } -} diff --git a/DecSm.Atom/Workflows/Model/WorkflowStepModel.cs b/DecSm.Atom/Workflows/Model/WorkflowStepModel.cs deleted file mode 100644 index 2f133ec2..00000000 --- a/DecSm.Atom/Workflows/Model/WorkflowStepModel.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace DecSm.Atom.Workflows.Model; - -/// -/// Represents a single step within a workflow job, defining its configuration and behavior. -/// -/// The name of the workflow step. -[PublicAPI] -public sealed record WorkflowStepModel(string Name) -{ - /// - /// Gets a value indicating whether artifact publishing should be suppressed for this step. - /// - /// - /// If true, any artifacts produced by this step will not be published. Defaults to false. - /// - public bool SuppressArtifactPublishing { get; init; } - - /// - /// Gets the matrix dimensions for running this step in multiple configurations. - /// - /// - /// A matrix allows a step to be executed multiple times with different configurations. - /// - public IReadOnlyList MatrixDimensions { get; init; } = []; - - /// - /// Gets the options that configure this step's behavior. - /// - public IReadOnlyList Options { get; init; } = []; -} diff --git a/DecSm.Atom/Workflows/Options/CustomStep.cs b/DecSm.Atom/Workflows/Options/CustomStep.cs deleted file mode 100644 index 47ad83cd..00000000 --- a/DecSm.Atom/Workflows/Options/CustomStep.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace DecSm.Atom.Workflows.Options; - -/// -/// An abstract base record for defining a custom step within a workflow. -/// -/// -/// Inherit from this record to create specific custom workflow steps. -/// -/// public sealed record MyCustomActionStep(string ActionSetting) : CustomStep; -/// -/// -[PublicAPI] -public abstract record CustomStep : IWorkflowOption -{ - /// - /// Gets the optional name of the custom step, used for identification or logging. - /// - public string? Name { get; init; } - - /// - /// Gets a value indicating whether multiple instances of this step are allowed in a workflow. Defaults to true. - /// - public bool AllowMultiple => true; -} diff --git a/DecSm.Atom/Workflows/Options/DeployToEnvironment.cs b/DecSm.Atom/Workflows/Options/DeployToEnvironment.cs deleted file mode 100644 index a62fae33..00000000 --- a/DecSm.Atom/Workflows/Options/DeployToEnvironment.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace DecSm.Atom.Workflows.Options; - -/// -/// A workflow option that specifies the name of the environment to deploy to. -/// -[PublicAPI] -public sealed record DeployToEnvironment : WorkflowOption; diff --git a/DecSm.Atom/_usings.cs b/DecSm.Atom/_usings.cs deleted file mode 100644 index 59979812..00000000 --- a/DecSm.Atom/_usings.cs +++ /dev/null @@ -1,50 +0,0 @@ -global using System.Collections.Concurrent; -global using System.ComponentModel; -global using System.Diagnostics; -global using System.Diagnostics.CodeAnalysis; -global using System.Globalization; -global using System.IO.Abstractions; -global using System.Linq.Expressions; -global using System.Numerics; -global using System.Reflection; -global using System.Runtime.CompilerServices; -global using System.Text; -global using System.Text.Json; -global using System.Text.Json.Serialization; -global using System.Text.RegularExpressions; -global using DecSm.Atom.Args; -global using DecSm.Atom.Artifacts; -global using DecSm.Atom.Build; -global using DecSm.Atom.Build.Definition; -global using DecSm.Atom.Build.Model; -global using DecSm.Atom.BuildInfo; -global using DecSm.Atom.Help; -global using DecSm.Atom.Hosting; -global using DecSm.Atom.Logging; -global using DecSm.Atom.Params; -global using DecSm.Atom.Paths; -global using DecSm.Atom.Process; -global using DecSm.Atom.Reports; -global using DecSm.Atom.Secrets; -global using DecSm.Atom.Util; -global using DecSm.Atom.Util.Scope; -global using DecSm.Atom.Variables; -global using DecSm.Atom.Workflows; -global using DecSm.Atom.Workflows.Definition; -global using DecSm.Atom.Workflows.Definition.Options; -global using DecSm.Atom.Workflows.Definition.Triggers; -global using DecSm.Atom.Workflows.Model; -global using DecSm.Atom.Workflows.Options; -global using DecSm.Atom.Workflows.Writer; -global using JetBrains.Annotations; -global using Microsoft.Extensions.Configuration; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.DependencyInjection.Extensions; -global using Microsoft.Extensions.Hosting; -global using Microsoft.Extensions.Logging; -global using Spectre.Console; -global using Spectre.Console.Rendering; - -[assembly: InternalsVisibleTo("DecSm.Atom.Tests")] -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] -[assembly: SuppressMessage("ReSharper", "LocalizableElement")] diff --git a/Directory.Build.props b/Directory.Build.props index b1ea4278..07ff6678 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ icon.png readme.md - An opinionated task and build automation framework# + An opinionated task and build automation framework true true @@ -14,17 +14,17 @@ enable true $(NoWarn);CA1873;CS1591;NU5104;RCS1001;RCS1003;RCS1123; + true - true true embedded true - - + + - + \ No newline at end of file diff --git a/DecSm.Atom.sln.DotSettings b/Invex.Atom.sln.DotSettings similarity index 100% rename from DecSm.Atom.sln.DotSettings rename to Invex.Atom.sln.DotSettings diff --git a/Invex.Atom.slnx b/Invex.Atom.slnx new file mode 100644 index 00000000..19d1cb16 --- /dev/null +++ b/Invex.Atom.slnx @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 313c8a73..9909b646 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,528 @@ # Atom -[![Validate](https://github.com/DecSmith42/atom/actions/workflows/Validate.yml/badge.svg)](https://github.com/DecSmith42/atom/actions/workflows/Validate.yml) -[![Build](https://github.com/DecSmith42/atom/actions/workflows/Build.yml/badge.svg)](https://github.com/DecSmith42/atom/actions/workflows/Build.yml) -[![Dependabot Updates](https://github.com/DecSmith42/atom/actions/workflows/dependabot/dependabot-updates/badge.svg)](https://github.com/DecSmith42/atom/actions/workflows/dependabot/dependabot-updates) -[![CodeQL](https://github.com/DecSmith42/atom/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/DecSmith42/atom/actions/workflows/github-code-scanning/codeql) -[![Automatic Dependency Submission](https://github.com/DecSmith42/atom/actions/workflows/dependency-graph/auto-submission/badge.svg)](https://github.com/DecSmith42/atom/actions/workflows/dependency-graph/auto-submission) +[![Validate](https://github.com/Invex-Games/atom/actions/workflows/Validate.yml/badge.svg)](https://github.com/Invex-Games/atom/actions/workflows/Validate.yml) +[![Build](https://github.com/Invex-Games/atom/actions/workflows/Build.yml/badge.svg)](https://github.com/Invex-Games/atom/actions/workflows/Build.yml) +[![Dependabot Updates](https://github.com/Invex-Games/atom/actions/workflows/dependabot/dependabot-updates/badge.svg)](https://github.com/Invex-Games/atom/actions/workflows/dependabot/dependabot-updates) Atom is an opinionated, type-safe build automation framework for .NET. It enables you to define your build logic in C#, debug it like standard code, and automatically generate CI/CD configuration files for GitHub Actions and Azure DevOps. ## Why Atom? -* **Zero Context Switching**: Write build logic in C# alongside your application code. -* **Intellisense & Debugging**: Step through your build process using your IDE. -* **CI/CD Agnostic**: Define logic once; Atom generates the YAML for GitHub and Azure DevOps. -* **Modular**: Pull in capabilities via NuGet packages (GitVersion, Azure KeyVault, etc.). -* **Source Generators**: Reduces boilerplate by automatically discovering targets and parameters. - -## Basic Example - -1. Create a new file `Build.cs` - - ```csharp - #:package DecSm.Atom@2.* - - [BuildDefinition] - [GenerateEntryPoint] - partial class Build : BuildDefinition - { - Target SayHello => t => t - .Executes(() => Logger.LogInformation("Hello, World!")); - } - ``` +### Zero Context Switching + +Write build logic in C# alongside your application code. + +### Intellisense & Debugging + +Step through your build process using your IDE. + +### CI/CD Agnostic + +Define logic once; Atom generates the YAML for GitHub and Azure DevOps. + +### Modular + +Pull in capabilities via NuGet packages (GitVersion, Azure KeyVault, etc.). + +### Source Generators -2. Execute `dotnet run Build.cs SayHello` +Reduces boilerplate by automatically discovering targets and parameters. + +## Examples + +### Hello World + +> [!NOTE] +> +> It is recommended to use the atom dotnet tool to invoke atom projects: +> +> `dotnet tool install -g Invex.Atom.Tool` +> +> ` atom ...` +> +> However, the dotnet cli can also be used directly: +> +> `dotnet run -- ...` + +1. Create a .NET 10 project ``` - 25-12-16 +10:00 DecSm.Atom.Build.BuildExecutor: - 22:46:01.754 INF Executing build - - SayHello - - 25-12-16 +10:00 SayHello | Build: - 22:46:01.790 INF Hello, World! - - Build Summary - - SayHello │ Succeeded │ <0.01s + dotnet new console -n _atom ``` -## Getting Started +2. Update the `_atom.csproj` file: + + ```xml + + + + net10.0 + Atom + + + + + + + + ``` + +3. Replace the `Program.cs` file with `IBuild.cs`: + + ```csharp + using Invex.Atom.Build.Definition; + using Invex.Atom.Build.Hosting; + + namespace Atom; + + [BuildDefinition] + [GenerateEntryPoint] + internal interface IBuild : IBuildDefinition + { + Target HelloWorld => + t => t + .DescribedAs("Prints a hello world message to the console") + .Executes(() => Logger.LogInformation("Hello, World!")); + } + ``` + +4. Execute `atom HelloWorld` + + ``` + 26-06-06 +10:00 Invex.Atom.Build.BuildExecutor: + 01:16:47.552 INF Executing build + + HelloWorld + + Prints a hello world message to the console + + 26-06-06 +10:00 HelloWorld | Atom.Build: + 01:16:47.616 INF Hello, World! + + + Build Summary + + HelloWorld │ Succeeded │ <0.01s + ``` + +### Adding Params + +1. Add a parameter to the `IBuild` interface: + + > [!NOTE] + > + > `.RequiresParam(nameof(...))` is used to ensure the parameter is provided before the target is executed. + > + > `.UsesParam(nameof(...))` is used to use the parameter if it is provided, but not fail the build if it is not. + + ```csharp + using Invex.Atom.Build.Definition; + using Invex.Atom.Build.Hosting; + using Invex.Atom.Build.Params; + + namespace Atom; + + [BuildDefinition] + [GenerateEntryPoint] + internal interface IBuild : IBuildDefinition + { + [ParamDefinition("my-name", "My name")] + string? MyName => GetParam(() => MyName); + + Target HelloWorld => + t => t + .DescribedAs("Prints a hello world message to the console") + .RequiresParam(nameof(MyName)) + .Executes(() => Logger.LogInformation("Hello, World! I am {MyName}.", MyName)); + } + ``` + +2. Execute `atom HelloWorld --my-name Frodo + + ``` + 26-06-06 +10:00 Invex.Atom.Build.BuildExecutor: + 01:23:25.405 INF Executing build + + HelloWorld + + Prints a hello world message to the console + + 26-06-06 +10:00 HelloWorld | Atom.Build: + 01:23:25.467 INF Hello, World! I am Frodo. + + + Build Summary + + HelloWorld │ Succeeded │ <0.01s + ``` + +### Adding Secrets + +1. Add a secret parameter to the `IBuild` interface: + + ```csharp + using Invex.Atom.Build.Definition; + using Invex.Atom.Build.Hosting; + using Invex.Atom.Build.Params; + + namespace Atom; + + [BuildDefinition] + [GenerateEntryPoint] + internal interface IBuild : IBuildDefinition + { + [ParamDefinition("my-name", "My name")] + string? MyName => GetParam(() => MyName); + + [SecretDefinition("my-secret", "My secret")] + string? MySecret => GetParam(() => MySecret); + + Target HelloWorld => + t => t + .DescribedAs("Prints a hello world message to the console") + .RequiresParam(nameof(MyName)) + .RequiresParam(nameof(MySecret)) + .Executes(() => + Logger.LogInformation("Hello, World! I am {MyName} and my secret is {MySecret}.", + MyName, + MySecret)); + } + ``` + +2. Execute `atom HelloWorld --my-name Frodo --my-secret TheOneRing` + + > [!NOTE] + > The secret is masked in the output. + + ``` + 26-06-06 +10:00 Invex.Atom.Build.BuildExecutor: + 01:27:08.482 INF Executing build + + HelloWorld + + Prints a hello world message to the console + + 26-06-06 +10:00 HelloWorld | Atom.Build: + 01:27:08.544 INF Hello, World! I am Frodo and my secret is *****. + + + Build Summary + + HelloWorld │ Succeeded │ <0.01s + ``` + +### Adding Target Dependencies -To get started with DecSm.Atom, follow the [Getting Started Guide](https://decsm42.gitbook.io/atom/getting-started). +1. Update the `IBuild` interface: + + ```csharp + using Invex.Atom.Build.Definition; + using Invex.Atom.Build.Hosting; + using Invex.Atom.Build.Params; + + namespace Atom; + + [BuildDefinition] + [GenerateEntryPoint] + internal interface IBuild : IBuildDefinition + { + [ParamDefinition("my-name", "My name")] + string? MyName => GetParam(() => MyName); + + [SecretDefinition("my-secret", "My secret")] + string? MySecret => GetParam(() => MySecret); + + Target HelloWorld => + t => t + .DescribedAs("Prints a hello world message to the console") + .RequiresParam(nameof(MyName)) + .RequiresParam(nameof(MySecret)) + .Executes(() => + Logger.LogInformation("Hello, World! I am {MyName} and my secret is {MySecret}.", MyName, MySecret)); + + Target Goodbye => + t => t + .DescribedAs("Prints a goodbye message to the console") + .DependsOn(nameof(HelloWorld)) + .Executes(() => Logger.LogInformation("Goodbye!")); + } + ``` + +2. Execute `atom Goodbye --my-name Frodo --my-secret TheOneRing` + + > [!NOTE] + > The `Goodbye` target depends on the `HelloWorld` target, so both will be executed in the correct order, and the + parameters only need to be provided once. + + ``` + 26-06-06 +10:00 Invex.Atom.Build.BuildExecutor: + 01:30:12.175 INF Executing build + + HelloWorld + + Prints a hello world message to the console + + 26-06-06 +10:00 HelloWorld | Atom.Build: + 01:30:12.235 INF Hello, World! I am Frodo and my secret is *****. + + Goodbye + + Prints a goodbye message to the console + + 26-06-06 +10:00 Goodbye | Atom.Build: + 01:30:12.240 INF Goodbye! + + + Build Summary + + HelloWorld │ Succeeded │ <0.01s + Goodbye │ Succeeded │ <0.01s + ``` + +### Adding Workflow Generation + +1. Update the `_atom.csproj` file: + + ```xml + + + + net10.0 + Atom + + + + + + + + ``` +2. Update the `IBuild` interface: + + ```csharp + using Invex.Atom.Build.BuildOptions; + using Invex.Atom.Build.Definition; + using Invex.Atom.Build.Hosting; + using Invex.Atom.Build.Params; + using Invex.Atom.Module.GithubWorkflows.Extensions; + using Invex.Atom.Module.GithubWorkflows.Helpers; + using Invex.Atom.Workflows; + using Invex.Atom.Workflows.Definition; + using Invex.Atom.Workflows.Definition.Triggers; + using Invex.Atom.Workflows.Options; + using Invex.StructuredText.Expressions; + + namespace Atom; + + [BuildDefinition] + [GenerateEntryPoint] + internal interface IBuild : IWorkflowBuildDefinition, IGithubWorkflows + { + [ParamDefinition("my-name", "My name")] + string? MyName => GetParam(() => MyName); + + [SecretDefinition("my-secret", "My secret")] + string? MySecret => GetParam(() => MySecret); + + Target HelloWorld => + t => t + .DescribedAs("Prints a hello world message to the console") + .RequiresParam(nameof(MyName)) + .RequiresParam(nameof(MySecret)) + .Executes(() => + Logger.LogInformation("Hello, World! I am {MyName} and my secret is {MySecret}.", MyName, MySecret)); + + Target Goodbye => + t => t + .DescribedAs("Prints a goodbye message to the console") + .DependsOn(nameof(HelloWorld)) + .Executes(() => Logger.LogInformation("Goodbye!")); + + IReadOnlyList IWorkflowBuildDefinition.Workflows => + [ + new("Hello") + { + Triggers = [WorkflowTriggers.PushToMain], + Targets = + [ + new(nameof(HelloWorld)) + { + Options = + [ + BuildOptions.Inject.Param(nameof(MyName), TextExpressions.Github.GithubRepositoryOwner), + BuildOptions.Inject.Secret(nameof(MySecret)), + ], + }, + new(nameof(Goodbye)), + ], + Types = [WorkflowTypes.Github.Action], + }, + ]; + } + ``` + +3. Execute `atom gen` + + ``` + 26-06-06 +10:00 Invex.Atom.Module.GithubWorkflows.GithubActions.GithubWorkflowFileWriter: + 01:38:57.388 INF Writing new workflow file: + L:\Repos\Invex-Games\atom\.github\workflows\Hello.yml + + + 26-06-06 +10:00 Invex.Atom.Build.BuildExecutor: + 01:38:57.472 INF Executing build + + Gen + + Generates workflow files + + + Build Summary + + Gen │ Succeeded │ <0.01s + ``` + + `.github/workflows/Hello.yml` + + ```yaml + name: Hello + + on: + push: + branches: [ main ] + + permissions: { } + + jobs: + + HelloWorld: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: HelloWorld + id: HelloWorld + run: dotnet run --project _atom/_atom.csproj -- HelloWorld --skip --headless + env: + my-secret: ${{ secrets.MY_SECRET }} + my-name: ${{ github.repository_owner }} + + Goodbye: + needs: [ HelloWorld ] + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Goodbye + id: Goodbye + run: dotnet run --project _atom/_atom.csproj -- Goodbye --skip --headless + ``` + +### File-based Apps + +Atom can also be used as a file-based app: + +`Atom.cs` + +```csharp +#:sdk Microsoft.NET.Sdk.Worker +#:package Invex.Atom.Build@3.* + +using Invex.Atom.Build.Definition; +using Invex.Atom.Build.Hosting; + +namespace Atom; + +[BuildDefinition] +[GenerateEntryPoint] +internal interface IBuild : IBuildDefinition +{ + Target HelloWorld => + t => t + .DescribedAs("Prints a hello world message to the console") + .Executes(() => Logger.LogInformation("Hello, World!")); +} +``` ## Documentation -Full documentation is available on [GitBook](https://decsm42.gitbook.io/atom/). +### Getting Started + +- [Introduction](docs/getting-started/introduction.md) — What Atom is and why you'd use it +- [Your First Build](docs/getting-started/your-first-build.md) — Create a minimal build and run it +- [Base vs Workflow Build](docs/getting-started/base-vs-workflow-build.md) — `BuildDefinition` vs + `WorkflowBuildDefinition` + +### Core Concepts + +- [Build Definitions](docs/core-concepts/build-definitions.md) — The `[BuildDefinition]` attribute and source generators +- [Targets](docs/core-concepts/targets.md) — Defining targets with the fluent `TargetDefinition` API +- [Parameters](docs/core-concepts/parameters.md) — Declaring, requiring, and resolving parameters +- [Secrets](docs/core-concepts/secrets.md) — Secure parameter handling with `ISecretsProvider` +- [Artifacts](docs/core-concepts/artifacts.md) — Producing and consuming build artifacts +- [Variables](docs/core-concepts/variables.md) — Sharing data between targets with workflow variables +- [File System](docs/core-concepts/file-system.md) — `IRootedFileSystem`, `RootedPath`, and path providers +- [Process Runner](docs/core-concepts/process-runner.md) — Executing external processes +- [Build Info](docs/core-concepts/build-info.md) — Build ID, version, and timestamp providers +- [Build Options](docs/core-concepts/build-options.md) — Configuring build behaviour with `IBuildOption` +- [Hosting](docs/core-concepts/hosting.md) — `AtomHost`, `[GenerateEntryPoint]`, and host configuration +- [Lifecycle Hooks](docs/core-concepts/lifecycle-hooks.md) — `IAtomLifecycleHook` for pre/post-build logic +- [Logging & Reports](docs/core-concepts/logging-and-reports.md) — Spectre console output and report data +- [File Transformations](docs/core-concepts/file-transformations.md) — Temporary, reversible file edits with + `TransformFileScope` + +### Workflows + +- [Overview](docs/workflows/overview.md) — What workflows add on top of a base build +- [Workflow Definitions](docs/workflows/workflow-definitions.md) — `WorkflowDefinition`, targets, and types +- [Triggers](docs/workflows/triggers.md) — Push, pull request, and manual triggers +- [Workflow Options](docs/workflows/workflow-options.md) — Checkout, deployment, conditions, and more +- [Variables in Workflows](docs/workflows/variables-in-workflows.md) — Cross-job data sharing +- [Debugging Workflows](docs/workflows/debugging-workflows.md) — Local workflow simulation + +### Modules + +- [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` +- [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` + +### Built-in Targets + +- [SetupBuildInfo](docs/built-in-targets/setup-build-info.md) — Initialises build ID, version, and timestamp +- [ValidateBuild](docs/built-in-targets/validate-build.md) — Checks the build for common issues +- [GenerateWorkflowFiles](docs/built-in-targets/generate-workflow-files.md) — Generates CI/CD YAML files + +### Developer Guide + +- [Writing a Module](docs/developer-guide/writing-a-module.md) — Creating a reusable Atom module NuGet package +- [Custom Providers](docs/developer-guide/custom-providers.md) — Implementing `IArtifactProvider`, `ISecretsProvider`, + 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` + +### Reference + +- [CLI](docs/reference/cli.md) — Command-line arguments and the Atom global tool +- [API Reference](api/index.md) — Auto-generated API documentation (via DocFX) + +## AI Disclaimer + +The Atom libraries are human-made, however GitHub Copilot completions have been used on a superficial level. + +Generative AI was also used to assist in writing tests and documentation. ## License diff --git a/Sample_01_HelloWorld/Sample_01_HelloWorld.csproj b/Sample_01_HelloWorld/Sample_01_HelloWorld.csproj deleted file mode 100644 index 0902eaaf..00000000 --- a/Sample_01_HelloWorld/Sample_01_HelloWorld.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net10.0 - Atom - - - - - - - - diff --git a/Sample_02_Params/Sample_02_Params.csproj b/Sample_02_Params/Sample_02_Params.csproj deleted file mode 100644 index 0902eaaf..00000000 --- a/Sample_02_Params/Sample_02_Params.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net10.0 - Atom - - - - - - - - diff --git a/_atom/Build.cs b/_atom/Build.cs deleted file mode 100644 index b4ceb538..00000000 --- a/_atom/Build.cs +++ /dev/null @@ -1,153 +0,0 @@ -namespace Atom; - -[BuildDefinition] -[GenerateEntryPoint] -[GenerateSolutionModel] -internal partial class Build : BuildDefinition, - IAzureKeyVault, - IDevopsWorkflows, - IGithubWorkflows, - IGitVersion, - IBuildTargets, - ITestTargets, - IDeployTargets, - IApproveDependabotPr -{ - public static readonly string[] PlatformNames = - [ - IJobRunsOn.WindowsLatestTag, - "windows-11-arm", - IJobRunsOn.UbuntuLatestTag, - "ubuntu-24.04-arm", - "macos-15-intel", - IJobRunsOn.MacOsLatestTag, - ]; - - public static readonly string[] DevopsPlatformNames = - [ - IJobRunsOn.WindowsLatestTag, IJobRunsOn.UbuntuLatestTag, IJobRunsOn.MacOsLatestTag, - ]; - - public static readonly string[] FrameworkNames = ["net8.0", "net9.0", "net10.0"]; - - private static readonly MatrixDimension TestFrameworkMatrix = new(nameof(ITestTargets.TestFramework)) - { - Values = FrameworkNames, - }; - - public override IReadOnlyList GlobalWorkflowOptions => - [ - UseAzureKeyVault.Enabled, UseGitVersionForBuildId.Enabled, new SetupDotnetStep("10.0.x"), - ]; - - public override IReadOnlyList Workflows => - [ - // Real workflows - new("Validate") - { - Triggers = [ManualTrigger.Empty, GitPullRequestTrigger.IntoMain], - Targets = - [ - WorkflowTargets.SetupBuildInfo, - WorkflowTargets.PackProjects.WithSuppressedArtifactPublishing, - WorkflowTargets.PackTool.WithSuppressedArtifactPublishing.WithGithubRunnerMatrix(PlatformNames), - WorkflowTargets - .TestProjects - .WithGithubRunnerMatrix(PlatformNames) - .WithMatrixDimensions(TestFrameworkMatrix) - .WithOptions(new SetupDotnetStep("8.0.x"), new SetupDotnetStep("9.0.x")), - ], - WorkflowTypes = [Github.WorkflowType], - Options = [GithubTokenPermissionsOption.NoneAll], - }, - new("Build") - { - Triggers = - [ - ManualTrigger.Empty, - new GitPushTrigger - { - IncludedBranches = ["main", "feature/**", "patch/**"], - }, - GithubReleaseTrigger.OnReleased, - ], - Targets = - [ - WorkflowTargets.SetupBuildInfo, - WorkflowTargets.PackProjects, - WorkflowTargets.PackTool.WithGithubRunnerMatrix(PlatformNames), - WorkflowTargets - .TestProjects - .WithGithubRunnerMatrix(PlatformNames) - .WithMatrixDimensions(TestFrameworkMatrix) - .WithOptions(new SetupDotnetStep("8.0.x"), new SetupDotnetStep("9.0.x")), - WorkflowTargets.PushToNuget, - WorkflowTargets - .PushToRelease - .WithGithubTokenInjection(new() - { - Contents = GithubTokenPermission.Write, - }) - .WithOptions(GithubIf.Create(new ConsumedVariableExpression(nameof(WorkflowTargets.SetupBuildInfo), - ParamDefinitions[nameof(ISetupBuildInfo.BuildVersion)].ArgName) - .Contains(new StringExpression("-")) - .EqualTo("false"))), - ], - WorkflowTypes = [Github.WorkflowType], - Options = [GithubTokenPermissionsOption.NoneAll], - }, - new("Dependabot Enable auto-merge") - { - Triggers = [GitPullRequestTrigger.IntoMain], - Targets = [WorkflowTargets.ApproveDependabotPr], - WorkflowTypes = [Github.WorkflowType], - Options = - [ - GithubTokenPermissionsOption.NoneAll, - GithubIf.Create(new EqualExpression("github.actor", new StringExpression("dependabot[bot]"))), - new WorkflowParamInjection(nameof(IApproveDependabotPr.PullRequestNumber), - new LiteralExpression("github.event.number").Expression), - WorkflowSecretInjection.Create(nameof(IApproveDependabotPr.DependabotEnableAutoMergePat)), - ], - }, - - // Test workflows - new("Test_Devops_Build") - { - Triggers = [ManualTrigger.Empty, GitPullRequestTrigger.IntoMain, GitPushTrigger.ToMain], - Targets = - [ - WorkflowTargets.SetupBuildInfo, - WorkflowTargets.PackProjects, - WorkflowTargets.PackTool.WithDevopsPoolMatrix(DevopsPlatformNames), - WorkflowTargets - .TestProjects - .WithDevopsPoolMatrix(DevopsPlatformNames) - .WithMatrixDimensions(TestFrameworkMatrix) - .WithOptions(new SetupDotnetStep("8.0.x"), new SetupDotnetStep("9.0.x")), - WorkflowTargets.PushToNugetDevops, - ], - WorkflowTypes = [Devops.WorkflowType], - Options = [new WorkflowParamInjection(Params.NugetDryRun, "true"), new DevopsVariableGroup("Atom")], - }, - Github.DependabotWorkflow(new() - { - Registries = [new("nuget", DependabotValues.NugetType, DependabotValues.NugetUrl)], - Updates = - [ - new(DependabotValues.NugetEcosystem) - { - Registries = ["nuget"], - Groups = - [ - new("nuget-deps") - { - Patterns = ["*"], - }, - ], - Schedule = DependabotSchedule.Daily, - }, - ], - }), - ]; -} diff --git a/_atom/IBuild.cs b/_atom/IBuild.cs new file mode 100644 index 00000000..e268b1bf --- /dev/null +++ b/_atom/IBuild.cs @@ -0,0 +1,315 @@ +namespace Atom; + +[BuildDefinition] +[GenerateEntryPoint] +[GenerateSolutionModel] +internal interface IBuild : IWorkflowBuildDefinition, + IDotnetUserSecrets, + IDevopsWorkflows, + IGithubWorkflows, + IGitVersion, + IBuildTargets, + ITestTargets, + IDeployTargets, + IApproveDependabotPr, + ICheckPrForBreakingChanges, + IDocTargets +{ + static readonly string[] PlatformNames = + [ + WorkflowLabels.Github.RunsOn.Windows_Latest, + WorkflowLabels.Github.RunsOn.Windows_11_Arm, + WorkflowLabels.Github.RunsOn.Ubuntu_Latest, + WorkflowLabels.Github.RunsOn.Ubuntu_24_04_Arm, + WorkflowLabels.Github.RunsOn.MacOs_15_Intel, + WorkflowLabels.Github.RunsOn.MacOs_Latest, + ]; + + static readonly string[] DevopsPlatformNames = + [ + WorkflowLabels.Devops.Pool.Windows_Latest, + WorkflowLabels.Devops.Pool.Ubuntu_Latest, + WorkflowLabels.Devops.Pool.MacOs_Latest, + ]; + + static readonly string[] FrameworkNames = + [ + WorkflowLabels.Dotnet.Framework.Net_8_0, + WorkflowLabels.Dotnet.Framework.Net_9_0, + WorkflowLabels.Dotnet.Framework.Net_10_0, + ]; + + IReadOnlyList IBuildDefinition.Options => + [ + BuildOptions.GitVersion.ProvideBuildId, + BuildOptions.GitVersion.ProvideBuildVersion, + BuildOptions.Steps.SetupDotnet.Dotnet100X(), + ]; + + IReadOnlyList IWorkflowBuildDefinition.Workflows => + [ + // Build / validate + new("Validate") + { + Triggers = [WorkflowTriggers.Manual, WorkflowTriggers.PullIntoMain], + Targets = + [ + new(nameof(SetupBuildInfo)), + new(nameof(PackProjects)) + { + Options = [BuildOptions.Target.SuppressArtifactPublishing], + }, + new(nameof(PackTool)) + { + MatrixDimensions = + [ + new(nameof(JobRunsOn)) + { + Values = PlatformNames, + }, + ], + Options = [BuildOptions.Target.SuppressArtifactPublishing, BuildOptions.Github.RunsOn.SetByMatrix], + }, + new(nameof(TestProjects)) + { + MatrixDimensions = + [ + new(nameof(JobRunsOn)) + { + Values = PlatformNames, + }, + new(nameof(TestFramework)) + { + Values = FrameworkNames, + }, + ], + Options = + [ + BuildOptions.Target.SuppressArtifactPublishing, + BuildOptions.Github.RunsOn.SetByMatrix, + BuildOptions.Steps.SetupDotnet.Dotnet80X(), + BuildOptions.Steps.SetupDotnet.Dotnet90X(), + ], + }, + new(nameof(BuildDocs)) + { + Options = [BuildOptions.Target.SuppressArtifactPublishing], + }, + new(nameof(CheckPrForBreakingChanges)) + { + 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, + })), + BuildOptions.Inject.Param(nameof(PullRequestNumber), + TextExpressions.Github.GithubEvent["number"]), + BuildOptions.Target.RunIfWorkflowCondition( + TextExpressions.Github.GithubEventName.EqualToString("pull_request")), + ], + }, + ], + Types = [WorkflowTypes.Github.Action], + }, + new("Build") + { + Triggers = + [ + WorkflowTriggers.Manual, + new GitPushTrigger + { + IncludedBranches = ["main", "feature/**", "patch/**"], + }, + new GithubTrigger(new On.Release([On.Release.ReleaseType.released])), + ], + Targets = + [ + new(nameof(SetupBuildInfo)), + new(nameof(PackProjects)), + new(nameof(PackTool)) + { + MatrixDimensions = + [ + new(nameof(JobRunsOn)) + { + Values = PlatformNames.ToList(), + }, + ], + Options = [BuildOptions.Github.RunsOn.SetByMatrix], + }, + new(nameof(TestProjects)) + { + 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(BuildDocs)), + new(nameof(PublishDocs)) + { + Options = + [ + BuildOptions.Inject.Secret(nameof(GithubToken)), + new GithubTokenPermissionsOption(new Permissions.Exact(new() + { + Contents = PermissionsLevel.Write, + })), + BuildOptions.Target.RunIfWorkflowCondition(TextExpressions + .Target + .ParamOutput(this, nameof(SetupBuildInfo), nameof(BuildVersion)) + .Contains("-") + .EqualTo(false)), + ], + }, + new(nameof(PushToNuget)) + { + Options = [BuildOptions.Inject.Secret(nameof(NugetApiKey))], + }, + new(nameof(PushToRelease)) + { + Options = + [ + BuildOptions.Inject.Secret(nameof(GithubToken)), + new GithubTokenPermissionsOption(new Permissions.Exact(new() + { + Contents = PermissionsLevel.Write, + })), + BuildOptions.Target.RunIfWorkflowCondition(TextExpressions + .Target + .ParamOutput(this, nameof(SetupBuildInfo), nameof(BuildVersion)) + .Contains("-") + .EqualTo(false)), + ], + }, + ], + Types = [WorkflowTypes.Github.Action], + }, + + // Test devops + new("Test_Devops_Build") + { + Triggers = [WorkflowTriggers.Manual, WorkflowTriggers.PullIntoMain, WorkflowTriggers.PushToMain], + Targets = + [ + new(nameof(SetupBuildInfo)), + new(nameof(PackProjects)), + new(nameof(PackTool)) + { + MatrixDimensions = + [ + new(nameof(JobRunsOn)) + { + Values = DevopsPlatformNames, + }, + ], + Options = [BuildOptions.Devops.DevopsPool.SetByMatrix], + }, + new(nameof(TestProjects)) + { + MatrixDimensions = + [ + new(nameof(JobRunsOn)) + { + Values = DevopsPlatformNames, + }, + new(nameof(TestFramework)) + { + Values = FrameworkNames, + }, + ], + Options = + [ + BuildOptions.Devops.DevopsPool.SetByMatrix, + BuildOptions.Steps.SetupDotnet.Dotnet80X(), + BuildOptions.Steps.SetupDotnet.Dotnet90X(), + ], + }, + new(nameof(PushToNugetDevops)) + { + Options = [BuildOptions.Inject.Secret(nameof(NugetApiKey))], + }, + ], + Types = [WorkflowTypes.Devops.Pipeline], + Options = [BuildOptions.Inject.Param(nameof(NugetDryRun), true), BuildOptions.Devops.VariableGroup.Atom], + }, + + // Dependabot + WorkflowPresets.Github.Dependabot(new() + { + Registries = new Dictionary + { + ["nuget"] = new() + { + Type = RegistryType.NugetFeed, + Url = WorkflowLabels.Github.Dependabot.NugetUrl, + }, + }, + Updates = + [ + new() + { + Directory = "/", + PackageEcosystem = WorkflowLabels.Github.Dependabot.NugetEcosystem, + Registries = new DependabotRegistries.Named("nuget"), + Groups = new Dictionary + { + ["nuget-deps"] = new DependabotGroup.FromPatterns + { + Patterns = ["*"], + }, + }, + Schedule = new() + { + Interval = ScheduleInterval.Daily, + }, + TargetBranch = "main", + OpenPullRequestsLimit = 10, + }, + ], + }), + new("Dependabot Enable auto-merge") + { + Triggers = [WorkflowTriggers.PullIntoMain], + Targets = + [ + new(nameof(ApproveDependabotPr)) + { + Options = + [ + BuildOptions.Inject.Secret(nameof(GithubToken)), + BuildOptions.Inject.Param(nameof(PullRequestNumber), + TextExpressions.Github.GithubEvent["number"]), + BuildOptions.Target.RunIfWorkflowCondition( + TextExpressions.Github.GithubActor.EqualToString("dependabot[bot]")), + new GithubTokenPermissionsOption(new Permissions.Exact(new() + { + IdTokens = PermissionsLevel.Write, + Contents = PermissionsLevel.Write, + PullRequests = PermissionsLevel.Write, + })), + ], + }, + ], + Types = [WorkflowTypes.Github.Action], + }, + ]; +} diff --git a/_atom/Properties/launchSettings.json b/_atom/Properties/launchSettings.json index 8f25d07c..7888886f 100644 --- a/_atom/Properties/launchSettings.json +++ b/_atom/Properties/launchSettings.json @@ -7,6 +7,22 @@ "environmentVariables": { "DOTNET_ENVIRONMENT": "Development" } + }, + "_atom gen": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + }, + "commandLineArgs": "gen" + }, + "_atom CheckPrForBreakingChanges": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + }, + "commandLineArgs": "CheckPrForBreakingChanges" } } } diff --git a/_atom/Targets/IApiSurfaceHelper.cs b/_atom/Targets/IApiSurfaceHelper.cs new file mode 100644 index 00000000..e0fc769f --- /dev/null +++ b/_atom/Targets/IApiSurfaceHelper.cs @@ -0,0 +1,95 @@ +namespace Atom.Targets; + +public sealed record BreakingChanges(IReadOnlyList MajorChanges, IReadOnlyList MinorChanges); + +public sealed record Change(RootedPath Path, List AddedLines, List DeletedLines); + +public interface IApiSurfaceHelper : IBuildAccessor +{ + BreakingChanges IdentifyBreakingChanges( + SemVer oldVersion, + string oldCommitHash, + SemVer newVersion, + string newCommitHash, + params RootedPath[] filesToCheck) + { + var filesToCheckDisplay = string.Join(", ", filesToCheck); + + Logger.LogDebug("Identifying breaking changes with options: {@Options}", + new + { + oldVersion, + oldCommitHash, + newVersion, + newCommitHash, + filesToCheck = filesToCheckDisplay, + }); + + var targetFiles = FormatTargetFiles(filesToCheck); + + using var repo = new Repository(RootedFileSystem.AtomRootDirectory); + var oldCommit = repo.Lookup(oldCommitHash); + + if (oldCommit?.IsMissing is not false) + throw new InvalidOperationException($"Commit {oldCommitHash} is missing."); + + var newCommit = repo.Lookup(newCommitHash); + + if (newCommit?.IsMissing is not false) + throw new InvalidOperationException($"Commit {newCommitHash} is missing."); + + var changes = repo.Diff.Compare(oldCommit.Tree, newCommit.Tree); + + Logger.LogDebug("Changes: {@Changes}", + new + { + changes.Content, + changes.LinesDeleted, + changes.LinesAdded, + }); + + if (changes is null or { LinesAdded: 0, LinesDeleted: 0 }) + return new([], []); + + IReadOnlyList suspiciousChanges = changes + .Where(x => targetFiles.Contains(x.Path) && x.LinesDeleted > 0) + .Select(x => new Change(RootedFileSystem.AtomRootDirectory / x.Path, x.AddedLines, x.DeletedLines)) + .ToList(); + + Logger.LogDebug("Suspicious changes: {@SuspiciousChanges}", suspiciousChanges); + + var majorChanges = suspiciousChanges + .Where(x => x.DeletedLines.Count > 0 && + x + .DeletedLines + .Select(l => l.Content.Trim()) + .All(deletedLine => !deletedLine.StartsWith(',') && !deletedLine.EndsWith(','))) + .ToList(); + + Logger.LogDebug("Major changes: {@MajorChanges}", majorChanges); + + var minorChanges = suspiciousChanges + .Except(majorChanges) + .Where(x => x.AddedLines.Count > 0) + .ToList(); + + Logger.LogDebug("Minor changes: {@MinorChanges}", minorChanges); + + return new(majorChanges, minorChanges); + } + + private HashSet FormatTargetFiles(RootedPath[] filesToCheck) + { + var targetFiles = filesToCheck + .Select(x => RootedFileSystem.Path.IsPathRooted(x) + ? RootedFileSystem.Path.GetRelativePath(RootedFileSystem.AtomRootDirectory, x) + : x) + .Select(x => x.Replace("\\", "/")) + .Select(x => x.StartsWith('/') + ? x[1..] + : x) + .ToHashSet(); + + return targetFiles; + } +} diff --git a/_atom/Targets/IApproveDependabotPr.cs b/_atom/Targets/IApproveDependabotPr.cs index 8abc5938..0e5064d6 100644 --- a/_atom/Targets/IApproveDependabotPr.cs +++ b/_atom/Targets/IApproveDependabotPr.cs @@ -1,12 +1,9 @@ namespace Atom.Targets; -public interface IApproveDependabotPr : IGithubHelper +public interface IApproveDependabotPr : IGithubHelper, IPullRequestHelper { const string DependabotActorName = "dependabot[bot]"; - [ParamDefinition("pull-request-number", "The pull request number to approve.")] - int PullRequestNumber => GetParam(() => PullRequestNumber); - [SecretDefinition("dependabot-enable-auto-merge-pat", "A GitHub PAT with permissions to enable auto-merge on pull requests.")] string? DependabotEnableAutoMergePat => GetParam(() => DependabotEnableAutoMergePat); diff --git a/_atom/Targets/IBuildTargets.cs b/_atom/Targets/IBuildTargets.cs index 4dadc76a..c4feb417 100644 --- a/_atom/Targets/IBuildTargets.cs +++ b/_atom/Targets/IBuildTargets.cs @@ -4,13 +4,14 @@ internal interface IBuildTargets : IDotnetPackHelper, IDotnetPublishHelper { static readonly string[] ProjectsToPack = [ - Projects.DecSm_Atom.Name, - Projects.DecSm_Atom_Module_AzureKeyVault.Name, - Projects.DecSm_Atom_Module_AzureStorage.Name, - Projects.DecSm_Atom_Module_DevopsWorkflows.Name, - Projects.DecSm_Atom_Module_Dotnet.Name, - Projects.DecSm_Atom_Module_GitVersion.Name, - Projects.DecSm_Atom_Module_GithubWorkflows.Name, + Projects.Invex_Atom_Build.Name, + Projects.Invex_Atom_Workflows.Name, + Projects.Invex_Atom_Module_AzureKeyVault.Name, + Projects.Invex_Atom_Module_AzureStorage.Name, + Projects.Invex_Atom_Module_DevopsWorkflows.Name, + Projects.Invex_Atom_Module_Dotnet.Name, + Projects.Invex_Atom_Module_GitVersion.Name, + Projects.Invex_Atom_Module_GithubWorkflows.Name, ]; Target PackProjects => @@ -26,14 +27,14 @@ internal interface IBuildTargets : IDotnetPackHelper, IDotnetPublishHelper Target PackTool => t => t .DescribedAs("Packs the Atom tool into a nuget package") - .ProducesArtifact(Projects.DecSm_Atom_Tool.Name) + .ProducesArtifact(Projects.Invex_Atom_Tool.Name) .Executes(async cancellationToken => { var runtimeIdentifier = RuntimeInformation.RuntimeIdentifier; Logger.LogInformation("Packing AOT Atom tool for runtime {RuntimeIdentifier}", runtimeIdentifier); - await DotnetPackAndStage(FileSystem.GetPath(), + await DotnetPackAndStage(RootedFileSystem.GetPath(), new() { PackOptions = new() @@ -51,7 +52,7 @@ await DotnetPackAndStage(FileSystem.GetPath(), { Logger.LogInformation("Packing Atom tool for non-native AOT"); - await DotnetPackAndStage(FileSystem.GetPath(), + await DotnetPackAndStage(RootedFileSystem.GetPath(), new() { ClearPublishDirectory = false, diff --git a/_atom/Targets/ICheckPrForBreakingChanges.cs b/_atom/Targets/ICheckPrForBreakingChanges.cs new file mode 100644 index 00000000..0ec72daa --- /dev/null +++ b/_atom/Targets/ICheckPrForBreakingChanges.cs @@ -0,0 +1,246 @@ +namespace Atom.Targets; + +public sealed record ReleaseInfo(string CommitHash, SemVer Version); + +public interface ICheckPrForBreakingChanges : IGithubHelper, IPullRequestHelper, ISetupBuildInfo, IApiSurfaceHelper +{ + private RootedPath[] FilesToCheck => + RootedFileSystem + .Directory + .GetFiles(RootedFileSystem.AtomRootDirectory / "tests", "*.verified.txt", SearchOption.AllDirectories) + .Select(RootedFileSystem.CreateRootedPath) + .ToArray(); + + Target CheckPrForBreakingChanges => + t => t + .RequiresParam(nameof(GithubToken), nameof(PullRequestNumber)) + .ConsumesVariable(nameof(SetupBuildInfo), nameof(BuildVersion)) + .Executes(async cancellationToken => + { + 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; + Logger.LogDebug("Current commit hash: {CommitHash}", currentCommitHash); + + var currentVersion = BuildVersion; + Logger.LogDebug("Current version: {Version}", currentVersion); + + var latestReleaseInfo = FindLatestReleaseInfo(repo, currentVersion); + Logger.LogDebug("Latest release info: {ReleaseInfo}", latestReleaseInfo); + + if (latestReleaseInfo is null) + { + Logger.LogInformation("No previous release found. Skipping breaking changes check."); + + return; + } + + Logger.LogInformation( + "Comparing current version {CurrentVersion} with latest release version {LatestVersion} to identify breaking changes.", + currentVersion, + latestReleaseInfo.Version); + + var breakingChanges = IdentifyBreakingChanges(latestReleaseInfo.Version, + latestReleaseInfo.CommitHash, + currentVersion, + currentCommitHash, + FilesToCheck); + + Logger.LogInformation( + "Identified {MajorCount} major breaking changes and {MinorCount} minor breaking changes.", + breakingChanges.MajorChanges.Count, + breakingChanges.MinorChanges.Count); + + var body = breakingChanges.MajorChanges.Count > 0 + ? currentVersion.Major > latestReleaseInfo.Version.Major + ? $""" + ℹ️ **Major Breaking Changes Detected** + + This pull request contains major breaking changes to the public API surface. + + **Version Bump Status:** ✅ Major version has been bumped from `{latestReleaseInfo.Version.Major}` to `{currentVersion.Major}` + + **Files with breaking changes:** + {string.Join("\n", breakingChanges.MajorChanges.Select(x => $"- `{x.Path}`"))} + + The major version has already been appropriately incremented to reflect these breaking changes. + """ + : $""" + ⚠️ **Major Breaking Changes Detected - Action Required** + + This pull request contains major breaking changes to the public API surface, but the major version has not been bumped. + + **Current Version:** `{currentVersion}` + **Latest Release:** `{latestReleaseInfo.Version}` + + **Files with breaking changes:** + {string.Join("\n", breakingChanges.MajorChanges.Select(x => $"- `{x.Path}` ({x.DeletedLines.Count} lines removed)"))} + + **Required Action:** Please increment the major version number before merging this pull request. + """ + : breakingChanges.MinorChanges.Count > 0 + ? currentVersion.Minor > latestReleaseInfo.Version.Minor + ? $""" + ℹ️ **Minor Breaking Changes Detected** + + This pull request contains minor breaking changes to the public API surface. + + **Version Bump Status:** ✅ Minor version has been bumped from `{latestReleaseInfo.Version.Minor}` to `{currentVersion.Minor}` + + **Files with breaking changes:** + {string.Join("\n", breakingChanges.MinorChanges.Select(x => $"- `{x.Path}`"))} + + The minor version has already been appropriately incremented to reflect these changes. + """ + : $""" + ⚠️ **Minor Breaking Changes Detected - Action Required** + + This pull request contains minor breaking changes to the public API surface, but the minor version has not been bumped. + + **Current Version:** `{currentVersion}` + **Latest Release:** `{latestReleaseInfo.Version}` + + **Files with breaking changes:** + {string.Join("\n", breakingChanges.MinorChanges.Select(x => $"- `{x.Path}` ({x.AddedLines.Count} lines added)"))} + + **Required Action:** Please increment the minor version number before merging this pull request. + """ + : """ + ✅ **No Breaking Changes Detected** + + This pull request does not contain any breaking changes to the public API surface. + Safe to merge without version bump considerations. + """; + + var hasInvalidChanges = breakingChanges switch + { + { MajorChanges.Count: > 0 } when currentVersion.Major <= latestReleaseInfo.Version.Major => true, + { MinorChanges.Count: > 0 } when currentVersion.Major <= latestReleaseInfo.Version.Major && + currentVersion.Minor <= latestReleaseInfo.Version.Minor => true, + _ => false, + }; + + Logger.LogInformation("Adding check status to pull request with status: {Status}", + hasInvalidChanges + ? "failure" + : "success"); + + await AddCheckStatus(owner, + hasInvalidChanges + ? "failure" + : "success", + body, + cancellationToken); + }); + + ReleaseInfo? FindLatestReleaseInfo(Repository repo, SemVer currentVersion) + { + var releaseVersions = repo + .Tags + .Select(x => new + { + Tag = x, + Version = !x.FriendlyName.StartsWith('v') + ? null + : !SemVer.TryParse(x.FriendlyName[1..], out var version) + ? null + : version, + }) + .Where(x => x.Version is not null && x.Version < currentVersion) + .Select(x => new + { + Tag = x.Tag!, + Version = x.Version!, + }) + .ToList(); + + if (releaseVersions.Count is 0) + { + Logger.LogWarning("No release found for current version {CurrentVersion}.", currentVersion); + + return null; + } + + var version = releaseVersions.MaxBy(x => x.Version)!; + + 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 a896764d..6814905b 100644 --- a/_atom/Targets/IDeployTargets.cs +++ b/_atom/Targets/IDeployTargets.cs @@ -15,7 +15,7 @@ internal interface IDeployTargets : INugetHelper, IGithubReleaseHelper, ISetupBu .RequiresParam(nameof(NugetApiKey)) .ConsumesVariable(nameof(SetupBuildInfo), nameof(BuildId)) .ConsumesArtifacts(nameof(IBuildTargets.PackProjects), IBuildTargets.ProjectsToPack) - .ConsumesArtifact(nameof(IBuildTargets.PackTool), Projects.DecSm_Atom_Tool.Name, PlatformNames) + .ConsumesArtifact(nameof(IBuildTargets.PackTool), Projects.Invex_Atom_Tool.Name, PlatformNames) .DependsOn(nameof(ITestTargets.TestProjects)) .Executes(async cancellationToken => { @@ -24,12 +24,12 @@ internal interface IDeployTargets : INugetHelper, IGithubReleaseHelper, ISetupBu await PushProject(project, NugetFeed, NugetApiKey, cancellationToken: cancellationToken); // Push Atom tool package - platform-specific + multi-targeted - foreach (var atomToolPackagePath in FileSystem.Directory.GetFiles( - FileSystem.AtomArtifactsDirectory / Projects.DecSm_Atom_Tool.Name, + foreach (var atomToolPackagePath in RootedFileSystem.Directory.GetFiles( + RootedFileSystem.AtomArtifactsDirectory / Projects.Invex_Atom_Tool.Name, "*.nupkg", SearchOption.AllDirectories)) await PushPackageToNuget( - FileSystem.AtomArtifactsDirectory / Projects.DecSm_Atom_Tool.Name / atomToolPackagePath, + RootedFileSystem.AtomArtifactsDirectory / Projects.Invex_Atom_Tool.Name / atomToolPackagePath, NugetFeed, NugetApiKey, cancellationToken: cancellationToken); @@ -42,7 +42,7 @@ await PushPackageToNuget( .RequiresParam(nameof(NugetApiKey)) .ConsumesVariable(nameof(SetupBuildInfo), nameof(BuildId)) .ConsumesArtifacts(nameof(IBuildTargets.PackProjects), IBuildTargets.ProjectsToPack) - .ConsumesArtifact(nameof(IBuildTargets.PackTool), Projects.DecSm_Atom_Tool.Name, DevopsPlatformNames) + .ConsumesArtifact(nameof(IBuildTargets.PackTool), Projects.Invex_Atom_Tool.Name, DevopsPlatformNames) .DependsOn(nameof(ITestTargets.TestProjects)) .Executes(() => Logger.LogInformation("Simulating push to Nuget feed")); @@ -52,7 +52,7 @@ await PushPackageToNuget( .RequiresParam(nameof(GithubToken)) .ConsumesVariable(nameof(SetupBuildInfo), nameof(BuildVersion)) .ConsumesArtifacts(nameof(IBuildTargets.PackProjects), IBuildTargets.ProjectsToPack) - .ConsumesArtifact(nameof(IBuildTargets.PackTool), Projects.DecSm_Atom_Tool.Name, PlatformNames) + .ConsumesArtifact(nameof(IBuildTargets.PackTool), Projects.Invex_Atom_Tool.Name, PlatformNames) .ConsumesArtifacts(nameof(ITestTargets.TestProjects), ITestTargets.ProjectsToTest, PlatformNames.SelectMany(platform => FrameworkNames.Select(framework => $"{platform}-{framework}"))) diff --git a/_atom/Targets/IDocTargets.cs b/_atom/Targets/IDocTargets.cs new file mode 100644 index 00000000..d05ab4fa --- /dev/null +++ b/_atom/Targets/IDocTargets.cs @@ -0,0 +1,236 @@ +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace Atom.Targets; + +internal interface IDocTargets : IDotnetCliHelper, IGithubHelper, ISetupBuildInfo +{ + private const string GeneratedDocsArtifactName = "GeneratedDocs"; + + Target BuildDocs => + t => t + .DescribedAs("Generates API documentation using DocFX") + .ProducesArtifact(GeneratedDocsArtifactName) + .Executes(async cancellationToken => + { + // First, build analyzers in release mode + await DotnetCli.Build([RootedFileSystem.GetPath()], + new() + { + Configuration = "Release", + }, + cancellationToken: cancellationToken); + + await DotnetCli.Build([RootedFileSystem.GetPath()], + new() + { + Configuration = "Release", + }, + cancellationToken: cancellationToken); + + var siteDirectory = RootedFileSystem.AtomRootDirectory / "_site"; + + // If .NET 10, we can just do dotnet tool exec docfx + if (RuntimeInformation.FrameworkDescription.StartsWith(".NET 10")) + { + await ProcessRunner.RunAsync(new("dotnet", "tool exec -y docfx") + { + WorkingDirectory = RootedFileSystem.AtomRootDirectory, + }, + cancellationToken); + } + else + { + // Otherwise, we need to restore the tools and run docfx + Logger.LogInformation("Acquiring DocFX tool..."); + + await ProcessRunner.RunAsync(new("dotnet", "tool update docfx -g") + { + WorkingDirectory = RootedFileSystem.AtomRootDirectory, + }, + cancellationToken); + + Logger.LogInformation("Running DocFX..."); + + await ProcessRunner.RunAsync(new("dotnet", "docfx") + { + WorkingDirectory = RootedFileSystem.AtomRootDirectory, + }, + cancellationToken); + } + + Logger.LogInformation("DocFX site generated at {Path}", siteDirectory); + + // Copy the generated site to the publish directory + await CopyDirectory(siteDirectory, RootedFileSystem.AtomPublishDirectory / GeneratedDocsArtifactName); + }); + + Target ServeDocs => + t => t + .DescribedAs("Builds and serves the DocFX documentation site locally") + .DependsOn(BuildDocs) + .ConsumesArtifact(nameof(BuildDocs), GeneratedDocsArtifactName) + .Executes(async cancellationToken => + { + Logger.LogInformation("Serving DocFX site at http://localhost:8080/README.html"); + Logger.LogInformation("Press Ctrl+C to stop the server."); + + try + { + await ProcessRunner.RunAsync(new("dotnet", "docfx serve _site --port 8080") + { + WorkingDirectory = RootedFileSystem.AtomRootDirectory, + }, + cancellationToken); + } + catch (TaskCanceledException) + { + Logger.LogInformation("DocFX server stopped."); + } + }); + + Target PublishDocs => + t => t + .DescribedAs("Publishes the generated DocFX site to GitHub Pages via the gh-pages branch") + .RequiresParam(nameof(GithubToken)) + .DependsOn(BuildDocs) + .DependsOn(SetupBuildInfo) + .ConsumesArtifact(nameof(BuildDocs), GeneratedDocsArtifactName) + .Executes(async cancellationToken => + { + var siteArtifact = RootedFileSystem.AtomArtifactsDirectory / GeneratedDocsArtifactName; + + if (!RootedFileSystem.Directory.Exists(siteArtifact)) + throw new StepFailedException("Site directory '_site' does not exist. Run BuildDocs first."); + + // Create a fresh temporary directory for the gh-pages checkout + var tempDir = RootedFileSystem.AtomTempDirectory / "gh-pages-temp"; + + if (RootedFileSystem.Directory.Exists(tempDir)) + ForceDeleteDirectory(tempDir); + + RootedFileSystem.Directory.CreateDirectory(tempDir); + + try + { + // Init a fresh repo + await ProcessRunner.RunAsync(new("git", "init") + { + WorkingDirectory = tempDir, + }, + cancellationToken); + + await ProcessRunner.RunAsync(new("git", "checkout --orphan gh-pages") + { + WorkingDirectory = tempDir, + }, + cancellationToken); + + // Set git user details for the commit + await ProcessRunner.RunAsync(new("git", ["config", "user.name", "\"github-actions[bot]\""]) + { + WorkingDirectory = tempDir, + }, + cancellationToken); + + await ProcessRunner.RunAsync(new("git", + ["config", "user.email", "\"41898282+github-actions[bot]@users.noreply.github.com\""]) + { + WorkingDirectory = tempDir, + }, + cancellationToken); + + // Copy the generated site to the gh-pages branch + await CopyDirectory(siteArtifact, tempDir); + + // Commit + await ProcessRunner.RunAsync(new("git", "add .") + { + WorkingDirectory = tempDir, + }, + cancellationToken); + + await ProcessRunner.RunAsync( + new("git", ["commit", "-m", "\"Deploy documentation to GitHub Pages\""]) + { + WorkingDirectory = tempDir, + }, + cancellationToken); + + // Get the remote URL from the main repo + var remoteResult = await ProcessRunner.RunAsync(new("git", "remote get-url origin") + { + WorkingDirectory = RootedFileSystem.AtomRootDirectory, + OutputLogLevel = LogLevel.Debug, + }, + cancellationToken); + + var remoteUrl = remoteResult.Output.Trim(); + + if (string.IsNullOrEmpty(remoteUrl)) + throw new StepFailedException("Could not determine git remote URL."); + + // Inject the GitHub token into the remote URL for authentication + if (!string.IsNullOrEmpty(GithubToken) && remoteUrl.StartsWith("https://")) + remoteUrl = remoteUrl.Replace("https://", $"https://x-access-token:{GithubToken}@"); + + // Force push to gh-pages + Logger.LogInformation("Pushing to gh-pages branch..."); + + await ProcessRunner.RunAsync(new("git", ["push", "--force", remoteUrl, "gh-pages"]) + { + WorkingDirectory = tempDir, + }, + cancellationToken); + + Logger.LogInformation("Documentation published to GitHub Pages successfully."); + } + finally + { + // Cleanup + if (RootedFileSystem.Directory.Exists(tempDir)) + ForceDeleteDirectory(tempDir); + } + }); + + /// + /// Copies all files and subdirectories from a source directory to a destination directory. + /// + /// The source directory. + /// The destination directory. + async Task CopyDirectory(RootedPath sourceDirectory, RootedPath destinationDirectory) + { + // Ensure the destination directory exists + RootedFileSystem.Directory.CreateDirectory(destinationDirectory); + + // Copy all files + foreach (var file in RootedFileSystem + .Directory + .GetFiles(sourceDirectory) + .Select(RootedFileSystem.CreateRootedPath)) + RootedFileSystem.File.Copy(file, destinationDirectory / RootedFileSystem.Path.GetFileName(file), true); + + // Recursively copy all subdirectories + foreach (var directory in RootedFileSystem + .Directory + .GetDirectories(sourceDirectory) + .Select(RootedFileSystem.CreateRootedPath)) + await CopyDirectory(directory, destinationDirectory / RootedFileSystem.Path.GetFileName(directory)); + } + + /// + /// Recursively removes read-only attributes and deletes a directory. + /// Git object files are marked read-only, so a plain Directory.Delete fails. + /// + void ForceDeleteDirectory(string path) + { + foreach (var file in RootedFileSystem.Directory.GetFiles(path, "*", SearchOption.AllDirectories)) + { + var attrs = RootedFileSystem.File.GetAttributes(file); + + if (attrs.HasFlag(FileAttributes.ReadOnly)) + RootedFileSystem.File.SetAttributes(file, attrs & ~FileAttributes.ReadOnly); + } + + RootedFileSystem.Directory.Delete(path, true); + } +} diff --git a/_atom/Targets/IPullRequestHelper.cs b/_atom/Targets/IPullRequestHelper.cs new file mode 100644 index 00000000..03be5ec3 --- /dev/null +++ b/_atom/Targets/IPullRequestHelper.cs @@ -0,0 +1,7 @@ +namespace Atom.Targets; + +public interface IPullRequestHelper : IBuildAccessor +{ + [ParamDefinition("pull-request-number", "The pull request number to approve.")] + int PullRequestNumber => GetParam(() => PullRequestNumber); +} diff --git a/_atom/Targets/ITestTargets.cs b/_atom/Targets/ITestTargets.cs index 0565b9ac..25a57f23 100644 --- a/_atom/Targets/ITestTargets.cs +++ b/_atom/Targets/ITestTargets.cs @@ -4,12 +4,13 @@ internal interface ITestTargets : IDotnetTestHelper { static readonly string[] ProjectsToTest = [ - Projects.DecSm_Atom_Tests.Name, - Projects.DecSm_Atom_Analyzers_Tests.Name, - Projects.DecSm_Atom_SourceGenerators_Tests.Name, - Projects.DecSm_Atom_Module_DevopsWorkflows_Tests.Name, - Projects.DecSm_Atom_Module_GithubWorkflows_Tests.Name, - Projects.DecSm_Atom_Tool_Tests.Name, + Projects.Invex_Atom_Build_Tests.Name, + 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_GithubWorkflows_Tests.Name, + Projects.Invex_Atom_Workflows_Tests.Name, + Projects.Invex_Atom_Tool_Tests.Name, ]; [ParamDefinition("test-framework", "Test framework to use for unit tests")] diff --git a/_atom/_atom.csproj b/_atom/_atom.csproj index 007dd62b..9fed7f5e 100644 --- a/_atom/_atom.csproj +++ b/_atom/_atom.csproj @@ -4,13 +4,21 @@ net10.0 Atom 661f5aa6-694c-4890-85c0-9b72f0bea988 + true + + + + + + + all @@ -27,13 +35,13 @@ - - - - - - - + + + + + + + diff --git a/_atom/_usings.cs b/_atom/_usings.cs index aa1fcb73..3961f3f5 100644 --- a/_atom/_usings.cs +++ b/_atom/_usings.cs @@ -1,21 +1,37 @@ global using System.Runtime.InteropServices; global using Atom.Targets; -global using DecSm.Atom; -global using DecSm.Atom.Build.Definition; -global using DecSm.Atom.Hosting; -global using DecSm.Atom.Module.AzureKeyVault; -global using DecSm.Atom.Module.DevopsWorkflows; -global using DecSm.Atom.Module.DevopsWorkflows.Generation.Options; -global using DecSm.Atom.Module.Dotnet.Helpers; -global using DecSm.Atom.Module.GithubWorkflows; -global using DecSm.Atom.Module.GithubWorkflows.Generation.Options; -global using DecSm.Atom.Module.GitVersion; -global using DecSm.Atom.Params; -global using DecSm.Atom.Paths; -global using DecSm.Atom.Workflows.Definition; -global using DecSm.Atom.Workflows.Definition.Options; -global using DecSm.Atom.Workflows.Definition.Triggers; -global using DecSm.Atom.Workflows.Options; +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.FileSystem; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Build.Params; +global using Invex.Atom.Build.Secrets; +global using Invex.Atom.Module.Dotnet.Helpers; +global using Invex.Atom.Module.GithubWorkflows; +global using Invex.Atom.Module.GithubWorkflows.Extensions; +global using Invex.Atom.Module.GithubWorkflows.Helpers; +global using Invex.Atom.Module.GitVersion; +global using Invex.Atom.Module.DevopsWorkflows; +global using Invex.Atom.Module.DevopsWorkflows.Extensions; +global using Invex.Atom.Module.GithubWorkflows.Options; +global using Invex.Atom.Module.GitVersion.Flags; +global using Invex.Atom.Workflows; +global using Invex.Atom.Workflows.Definition; +global using Invex.Atom.Workflows.Definition.Triggers; +global using Invex.Atom.Workflows.Dotnet; +global using Invex.Atom.Workflows.Extensions; +global using Invex.Atom.Workflows.Options; +global using Invex.FileSystem; +global using Invex.SemanticVersion; +global using Invex.StructuredText.Expressions; +global using Invex.StructuredText.GithubActions.DependabotConfigModel.Model; +global using Invex.StructuredText.GithubActions.GithubActionModel; +global using LibGit2Sharp; +global using Microsoft.Extensions.Logging; global using Octokit.GraphQL; global using Octokit.GraphQL.Internal; global using Octokit.GraphQL.Model; +global using Commit = LibGit2Sharp.Commit; +global using Repository = LibGit2Sharp.Repository; diff --git a/_atom/appsettings.json b/_atom/appsettings.json deleted file mode 100644 index 1a09d28e..00000000 --- a/_atom/appsettings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Params": { - "azure-vault-address": "https://dec-vault.vault.azure.net/" - } -} \ No newline at end of file diff --git a/api/index.md b/api/index.md new file mode 100644 index 00000000..8c646b62 --- /dev/null +++ b/api/index.md @@ -0,0 +1,19 @@ +# API Reference + +This section contains auto-generated API documentation for all public types in the Atom framework. + +## Packages + +| Package | Description | +|-------------------------------------|------------------------------------------------------------------| +| `Invex.Atom.Build` | Core framework — build definitions, targets, parameters, hosting | +| `Invex.Atom.Workflows` | Workflow definitions, triggers, and YAML generation | +| `Invex.Atom.Module.Dotnet` | .NET CLI helpers | +| `Invex.Atom.Module.GithubWorkflows` | GitHub Actions workflow writer | +| `Invex.Atom.Module.DevopsWorkflows` | Azure DevOps Pipelines workflow writer | +| `Invex.Atom.Module.AzureKeyVault` | Azure Key Vault secrets provider | +| `Invex.Atom.Module.AzureStorage` | Azure Blob Storage artifact provider | +| `Invex.Atom.Module.GitVersion` | GitVersion-based build ID and version | + +Browse the namespaces in the sidebar to explore the full API. + diff --git a/docfx.json b/docfx.json new file mode 100644 index 00000000..134977d1 --- /dev/null +++ b/docfx.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json", + "metadata": [ + { + "src": [ + { + "files": [ + "src/Invex.Atom.Build/Invex.Atom.Build.csproj", + "src/Invex.Atom.Workflows/Invex.Atom.Workflows.csproj", + "src/Invex.Atom.FileSystem/Invex.Atom.FileSystem.csproj", + "src/Invex.Atom.Process/Invex.Atom.Process.csproj", + "src/Invex.Atom.SemanticVersion/Invex.Atom.SemanticVersion.csproj", + "src/Invex.Atom.Module.AzureKeyVault/Invex.Atom.Module.AzureKeyVault.csproj", + "src/Invex.Atom.Module.AzureStorage/Invex.Atom.Module.AzureStorage.csproj", + "src/Invex.Atom.Module.DevopsWorkflows/Invex.Atom.Module.DevopsWorkflows.csproj", + "src/Invex.Atom.Module.Dotnet/Invex.Atom.Module.Dotnet.csproj", + "src/Invex.Atom.Module.GithubWorkflows/Invex.Atom.Module.GithubWorkflows.csproj", + "src/Invex.Atom.Module.GitVersion/Invex.Atom.Module.GitVersion.csproj" + ] + } + ], + "dest": "api", + "properties": { + "TargetFramework": "net10.0" + }, + "filter": "docs/filterConfig.yml" + } + ], + "build": { + "content": [ + { + "files": [ + "toc.yml", + "index.md", + "README.md" + ] + }, + { + "files": [ + "api/**.yml", + "api/index.md" + ] + }, + { + "files": [ + "docs/toc.yml", + "docs/**/*.md" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ] + } + ], + "output": "_site", + "globalMetadataFiles": [], + "fileMetadataFiles": [], + "globalMetadata": { + "_appTitle": "Atom", + "_appName": "Atom", + "_appFooter": "Atom — Build Automation for .NET", + "_enableSearch": true, + "_disableContribution": false, + "_disableToc": false, + "_gitContribute": { + "repo": "https://github.com/Invex-Games/atom", + "branch": "main" + } + }, + "template": [ + "default", + "modern" + ], + "postProcessors": [], + "keepFileLink": false, + "disableGitFeatures": false + } +} + + diff --git a/docs/built-in-targets/generate-workflow-files.md b/docs/built-in-targets/generate-workflow-files.md new file mode 100644 index 00000000..ff9405f9 --- /dev/null +++ b/docs/built-in-targets/generate-workflow-files.md @@ -0,0 +1,46 @@ +# GenerateWorkflowFiles + +**Interface:** `IGenerateWorkflowFiles` +**Package:** `Invex.Atom.Workflows` + +The `GenerateWorkflowFiles` target generates CI/CD configuration files from your workflow definitions. + +## Alias + +`Gen` — you can run this target with `dotnet run -- Gen`. + +## What It Does + +1. Resolves each `WorkflowDefinition` in your build's `Workflows` property. +2. Analyses the target dependency graph, artifact dependencies, and variable dependencies. +3. Maps targets into jobs and creates a platform-neutral `WorkflowModel`. +4. Invokes each registered `WorkflowFileWriter` (one per platform type) to render and write the YAML files. + +## Generated File Locations + +| Platform | Output Directory | +|----------------|------------------------------| +| GitHub Actions | `.github/workflows/` | +| Azure DevOps | Project root (pipeline YAML) | + +## Usage + +```shell +dotnet run -- GenerateWorkflowFiles +# or +dotnet run -- Gen +``` + +## Requirements + +Your build must: + +1. Inherit from `WorkflowBuildDefinition` (not just `BuildDefinition`). +2. Override the `Workflows` property with at least one definition. +3. Implement a platform module (`IGithubWorkflows`, `IDevopsWorkflows`, or both). + +## Outdated File Detection + +The `WorkflowLifecycleHook` checks on every build whether the files on disk are up to date with the current definitions. +If not, it warns or fails the build, prompting you to re-run `Gen`. + diff --git a/docs/built-in-targets/setup-build-info.md b/docs/built-in-targets/setup-build-info.md new file mode 100644 index 00000000..ce846065 --- /dev/null +++ b/docs/built-in-targets/setup-build-info.md @@ -0,0 +1,49 @@ +# SetupBuildInfo + +**Interface:** `ISetupBuildInfo` +**Package:** `Invex.Atom.Build` + +The `SetupBuildInfo` target initialises the build's identity — name, ID, version, and timestamp — and makes them +available as workflow variables. + +## What It Does + +1. Reads `BuildName`, `BuildId`, `BuildVersion`, and `BuildTimestamp` from the + registered [build info providers](../core-concepts/build-info.md). +2. Writes each as a workflow variable: `BuildName`, `BuildId`, `BuildVersion`, `BuildTimestamp`. +3. Adds a "Run Information" section to the build report. +4. Logs the values. + +## Produced Variables + +| Variable | Description | +|------------------|--------------------------------| +| `BuildName` | The logical name of the build | +| `BuildId` | Unique identifier for this run | +| `BuildVersion` | Semantic version string | +| `BuildTimestamp` | Timestamp as a string | + +## Usage + +Include `SetupBuildInfo` as a dependency of your first workflow target: + +```csharp +public override IReadOnlyList Workflows => +[ + new("CI") + { + Targets = + [ + new(nameof(ISetupBuildInfo.SetupBuildInfo)), + new(nameof(Compile)), + new(nameof(Test)), + ], + // ... + }, +]; +``` + +## Visibility + +This target is marked as **hidden** — it doesn't appear in default help output but is still executable. + diff --git a/docs/built-in-targets/validate-build.md b/docs/built-in-targets/validate-build.md new file mode 100644 index 00000000..46e3f8fd --- /dev/null +++ b/docs/built-in-targets/validate-build.md @@ -0,0 +1,40 @@ +# ValidateBuild + +**Interface:** `IValidateBuild` +**Package:** `Invex.Atom.Build` + +The `ValidateBuild` target checks the Atom build configuration for common issues. + +## What It Does + +1. Inspects all targets in the build model. +2. Reports **warnings** for targets without descriptions. +3. Reports **errors** for critical configuration problems (extensible). +4. Adds warning/error lists to the build report. +5. Throws `StepFailedException` if any errors are found. + +## Usage + +Run directly: + +```shell +dotnet run -- ValidateBuild +``` + +Or include as a dependency: + +```csharp +Target MyTarget => t => t + .DependsOn(nameof(IValidateBuild.ValidateBuild)) + .Executes(() => { /* ... */ }); +``` + +## Output + +Warnings appear in the build report. Example: + +``` +Warnings: + - Target 'MyTarget' has no description. +``` + diff --git a/docs/core-concepts/artifacts.md b/docs/core-concepts/artifacts.md new file mode 100644 index 00000000..ad2fa8fa --- /dev/null +++ b/docs/core-concepts/artifacts.md @@ -0,0 +1,86 @@ +# Artifacts + +Artifacts are files or directories produced by one target and consumed by another. Atom tracks artifact dependencies to +ensure correct execution order, especially in workflow scenarios where different targets may run on different machines. + +## Producing Artifacts + +Declare that a target produces an artifact: + +```csharp +Target Pack => t => t + .DescribedAs("Packs NuGet packages") + .ProducesArtifact("packages") + .Executes(async () => + { + // write files to the artifact directory + }); +``` + +You can produce multiple artifacts: + +```csharp +.ProducesArtifacts(["packages", "binaries"]) +``` + +## Consuming Artifacts + +Declare that a target consumes an artifact from another target: + +```csharp +Target Deploy => t => t + .ConsumesArtifact(nameof(Pack), "packages") + .Executes(async () => + { + // read files from the artifact directory + }); +``` + +Multiple artifacts from the same or different targets: + +```csharp +.ConsumesArtifacts(nameof(Pack), ["packages", "binaries"]) +``` + +## Build Slices + +Artifacts can be scoped to a **build slice** — a named variation within a matrix build (e.g. a specific OS or +framework): + +```csharp +.ProducesArtifact("tool", buildSlice: "windows-x64") +.ConsumesArtifact(nameof(PackTool), "tool", buildSlice: "windows-x64") +``` + +You can also consume an artifact across all slices: + +```csharp +.ConsumesArtifact(nameof(PackTool), "tool", buildSlices: ["windows-x64", "linux-x64"]) +``` + +## `IArtifactProvider` + +The actual storage and retrieval is handled by an `IArtifactProvider`. The default provider uses the local file system. +Modules can register alternative providers: + +| Provider | Package | Storage | +|-----------------------------|----------------------------------|--------------------| +| (default) | `Invex.Atom.Build` | Local file system | +| `AzureBlobArtifactProvider` | `Invex.Atom.Module.AzureStorage` | Azure Blob Storage | + +### Provider API + +```csharp +public interface IArtifactProvider +{ + Task StoreArtifacts(IEnumerable artifactNames, ...); + Task RetrieveArtifacts(IEnumerable artifactNames, ...); + Task Cleanup(IEnumerable runIdentifiers, ...); + Task> GetStoredRunIdentifiers(...); +} +``` + +## Next Steps + +→ [Variables](variables.md) + diff --git a/docs/core-concepts/build-definitions.md b/docs/core-concepts/build-definitions.md new file mode 100644 index 00000000..0a1410f8 --- /dev/null +++ b/docs/core-concepts/build-definitions.md @@ -0,0 +1,95 @@ +# Build Definitions + +A **build definition** is the central class of an Atom build. It declares which targets exist, what parameters are +available, and how the build host is configured. + +## The `[BuildDefinition]` Attribute + +Every build definition class must be: + +1. Decorated with `[BuildDefinition]` +2. Marked `partial` (so source generators can augment it) +3. Derived from `BuildDefinition` (or `WorkflowBuildDefinition`) + +```csharp +[BuildDefinition] +[GenerateEntryPoint] +internal partial class Build : BuildDefinition +{ + // targets, parameters, etc. +} +``` + +## What the Source Generator Does + +When you apply `[BuildDefinition]`, the Atom source generator automatically: + +- Discovers all `Target` properties (including those from implemented interfaces) and populates the `TargetDefinitions` + dictionary. +- Discovers all `[ParamDefinition]` / `[SecretDefinition]` properties and populates the `ParamDefinitions` dictionary. +- Generates the `AccessParam` method so the framework can inspect parameter values. + +This means you never need to manually register targets or parameters — just declare them and the generator wires +everything up. + +## `[GenerateEntryPoint]` + +Adding this attribute causes the source generator to emit a `Program.cs` with a `Main` method: + +```csharp +// Auto-generated +AtomHost.Run(args); +``` + +If you need custom host configuration, omit `[GenerateEntryPoint]` and write your own entry point: + +```csharp +var builder = AtomHost.CreateAtomBuilder(args); +// customise builder.Services, builder.Configuration, etc. +builder.Build().UseAtom().Run(); +``` + +## Composing with Interfaces + +Targets and parameters are typically defined in **interfaces** so they can be shared across builds or published in +module packages: + +```csharp +public interface IMyTargets : IBuildAccessor +{ + [ParamDefinition("greeting", "The greeting message")] + string Greeting => GetParam(() => Greeting, "Hello"); + + Target SayGreeting => t => t + .DescribedAs("Says a greeting") + .Executes(() => Logger.LogInformation(Greeting)); +} + +[BuildDefinition] +[GenerateEntryPoint] +internal partial class Build : BuildDefinition, IMyTargets { } +``` + +The source generator discovers `SayGreeting` and `Greeting` from `IMyTargets` and includes them in the build model. + +## `[GenerateSolutionModel]` + +When applied alongside `[BuildDefinition]`, this attribute generates a typed model of your solution's projects, giving +you compile-time access to project paths. + +## `ConfigureDefinitionHost` + +Override `ConfigureDefinitionHost` on your build class to register additional services before the build runs: + +```csharp +public override void ConfigureDefinitionHost(IHostApplicationBuilder builder) +{ + base.ConfigureDefinitionHost(builder); + builder.Services.AddSingleton(); +} +``` + +## Next Steps + +→ [Targets](targets.md) + diff --git a/docs/core-concepts/build-info.md b/docs/core-concepts/build-info.md new file mode 100644 index 00000000..564bb966 --- /dev/null +++ b/docs/core-concepts/build-info.md @@ -0,0 +1,54 @@ +# Build Info + +Atom provides a pluggable system for determining the build's identity: its **name**, **ID**, **version**, and * +*timestamp**. + +## `IBuildInfo` + +The `IBuildInfo` interface exposes: + +| Property | Type | Description | +|------------------|------------------|------------------------------------------------| +| `BuildName` | `string` | Logical name of the build (e.g. `"MyProject"`) | +| `BuildId` | `string` | Unique identifier for this build run | +| `BuildVersion` | `string` | Semantic version string | +| `BuildTimestamp` | `DateTimeOffset` | When the build started | + +## Providers + +Each piece of build info is resolved by a dedicated provider interface: + +| Interface | Default | Description | +|---------------------------|---------------------------------|---------------------------------| +| `IBuildIdProvider` | `DefaultBuildIdProvider` | Returns a GUID-based ID | +| `IBuildVersionProvider` | `DefaultBuildVersionProvider` | Returns `"0.0.0"` | +| `IBuildTimestampProvider` | `DefaultBuildTimestampProvider` | Returns `DateTimeOffset.UtcNow` | + +### Replacing a Provider + +Register your own implementation in the DI container. For example, the **GitVersion** module replaces both the ID and +version providers: + +```csharp +builder.Services + .AddSingleton() + .AddSingleton(); +``` + +## `SetupBuildInfo` Target + +The built-in `ISetupBuildInfo` interface provides a `SetupBuildInfo` target that: + +1. Reads the build name, ID, version, and timestamp from the providers. +2. Writes each as a [workflow variable](variables.md) (`BuildName`, `BuildId`, `BuildVersion`, `BuildTimestamp`). +3. Adds a "Run Information" section to the build report. +4. Logs the values. + +Most builds include `SetupBuildInfo` as a dependency of their first real target. + +See [SetupBuildInfo](../built-in-targets/setup-build-info.md) for details. + +## Next Steps + +→ [Build Options](build-options.md) + diff --git a/docs/core-concepts/build-options.md b/docs/core-concepts/build-options.md new file mode 100644 index 00000000..bf950970 --- /dev/null +++ b/docs/core-concepts/build-options.md @@ -0,0 +1,49 @@ +# Build Options + +Build options are configuration flags that modify the behaviour of the build or workflow at definition time. + +## `IBuildOption` + +All build options implement the marker interface `IBuildOption`. Options can be applied at three levels: + +1. **Build-wide** — via the `Options` property on your build definition +2. **Workflow-wide** — via `WorkflowDefinition.Options` +3. **Per-target** — via `WorkflowTargetDefinition.Options` + +```csharp +public override IReadOnlyList Options => +[ + BuildOptions.GitVersion.ProvideBuildId, + BuildOptions.GitVersion.ProvideBuildVersion, +]; +``` + +## `ToggleBuildOption` + +A simple on/off option: + +```csharp +BuildOptions.Target.SuppressArtifactPublishing +``` + +## Common Options + +Options are accessed via the static `BuildOptions` class, which is extended by modules. Examples: + +| Option | Description | +|--------------------------------------------------|---------------------------------------------| +| `BuildOptions.Target.SuppressArtifactPublishing` | Prevents artifact upload for a target | +| `BuildOptions.Steps.SetupDotnet.Dotnet80X()` | Adds a setup step to install .NET 8 | +| `BuildOptions.Github.RunsOn.SetByMatrix` | Sets the runner from the matrix dimension | +| `BuildOptions.Inject.Param(name, value)` | Injects a parameter value at workflow level | +| `BuildOptions.Inject.Secret(name)` | Injects a secret from the CI platform | + +## `IBuildOptionProvider` + +Modules can contribute options dynamically by implementing `IBuildOptionProvider`. The framework collects all providers +and merges their options at build time. + +## Next Steps + +→ [Hosting](hosting.md) + diff --git a/docs/core-concepts/file-system.md b/docs/core-concepts/file-system.md new file mode 100644 index 00000000..b1a82791 --- /dev/null +++ b/docs/core-concepts/file-system.md @@ -0,0 +1,84 @@ +# File System + +Atom provides `IRootedFileSystem` — a build-aware file system abstraction that layers path resolution on top of +`System.IO.Abstractions.IFileSystem`. + +> [!NOTE] +> The rooted file system types (`IRootedFileSystem`, `RootedPath`, `IPathProvider`, `TransformFileScope`, etc.) ship in +> the standalone [`Invex.FileSystem`](https://www.nuget.org/packages/Invex.FileSystem) package, maintained in its own +> repository. You don't need to reference it directly — it is pulled in transitively by `Invex.Atom.Build` and surfaced +> through `IBuildAccessor.RootedFileSystem`. + +## `IRootedFileSystem` + +Available via `IBuildAccessor.RootedFileSystem`, this interface gives you: + +- All standard `IFileSystem` operations (read, write, delete, etc.) +- **Path resolution** via `GetPath(key)` — looks up well-known paths by key +- A `CurrentDirectory` property returning a `RootedPath` + +### Resolving Well-Known Paths + +```csharp +var root = RootedFileSystem.GetPath("Root"); +var artifacts = RootedFileSystem.GetPath("Artifacts"); +var publish = RootedFileSystem.GetPath("Publish"); +``` + +Paths are resolved by querying registered `IPathProvider` implementations in priority order and are cached after first +resolution. + +## `RootedPath` + +`RootedPath` is a value type that wraps an absolute directory/file path and is bound to an `IRootedFileSystem` instance. +It supports the `/` operator for path combination: + +```csharp +var projectDir = RootedFileSystem.GetPath("Root") / "src" / "MyProject"; +var csproj = projectDir / "MyProject.csproj"; +``` + +Because a `RootedPath` carries a reference to the file system, you can perform I/O directly: + +```csharp +var content = RootedFileSystem.File.ReadAllText(csproj); +``` + +## `IPathProvider` + +Implement `IPathProvider` to register custom well-known paths: + +```csharp +public interface IPathProvider +{ + RootedPath? GetPath(string key); +} +``` + +Return `null` if your provider doesn't handle the requested key — the next provider in the chain will be tried. + +### Built-in Path Keys + +| Key | Description | +|-------------|----------------------------| +| `Root` | Repository / solution root | +| `Artifacts` | Build artifacts directory | +| `Publish` | Publish output directory | + +## `IPathMarker` + +For statically determined paths (e.g. source-generated project paths), implement `IPathMarker`: + +```csharp +public interface IPathMarker +{ + static abstract RootedPath Path(IRootedFileSystem fileSystem); +} +``` + +Resolve with `RootedFileSystem.GetPath()`. + +## Next Steps + +→ [Process Runner](process-runner.md) + diff --git a/docs/core-concepts/file-transformations.md b/docs/core-concepts/file-transformations.md new file mode 100644 index 00000000..8b275d72 --- /dev/null +++ b/docs/core-concepts/file-transformations.md @@ -0,0 +1,70 @@ +# File Transformations + +`TransformFileScope` provides disposable scopes for performing temporary, reversible changes to file content. This is +useful when a build step needs a file modified temporarily (e.g. patching a version in a `.csproj`) without permanently +altering it. + +> [!NOTE] +> `TransformFileScope` and `TransformMultiFileScope` ship in the standalone +> [`Invex.FileSystem`](https://www.nuget.org/packages/Invex.FileSystem) package, maintained in its own repository. It is +> pulled in transitively by `Invex.Atom.Build`, so no direct reference is required. + +## How It Works + +1. The file's current content is captured. +2. A transform function is applied and the result is written to disk. +3. When the scope is disposed, the original content is restored. + +If the file didn't exist before the scope was opened, it is deleted on disposal. + +## Async Usage + +```csharp +await using var scope = await TransformFileScope.CreateAsync( + projectFile, + content => content.Replace("1.0.0", buildVersion)); + +// File now contains the patched version. +// Build or pack the project here. + +// On disposal, the original content is restored. +``` + +## Sync Usage + +```csharp +using var scope = TransformFileScope.Create( + projectFile, + content => content.Replace("1.0.0", buildVersion)); +``` + +## Chaining Transforms + +Apply additional transforms within the same scope: + +```csharp +var scope = await TransformFileScope.CreateAsync(file, c => c.Replace("a", "b")); +await scope.AddAsync(c => c.Replace("x", "y")); +``` + +Disposal still restores to the **original** content (before the first transform), not the intermediate state. + +## Committing Changes + +Call `CancelRestore()` to make the transform permanent — the file will **not** be restored on disposal: + +```csharp +await using var scope = await TransformFileScope.CreateAsync(file, transform); +// ... verify the transform is correct ... +scope.CancelRestore(); // file keeps the transformed content +``` + +## `TransformMultiFileScope` + +For transforming multiple files atomically, use `TransformMultiFileScope` which manages a collection of +`TransformFileScope` instances. + +## Next Steps + +→ [Workflows Overview](../workflows/overview.md) + diff --git a/docs/core-concepts/hosting.md b/docs/core-concepts/hosting.md new file mode 100644 index 00000000..0f0430ab --- /dev/null +++ b/docs/core-concepts/hosting.md @@ -0,0 +1,84 @@ +# Hosting + +Atom uses the standard .NET `IHost` / `HostApplicationBuilder` infrastructure under the hood. The `AtomHost` class +provides convenience methods for setting up and running the host. + +## `AtomHost.Run(args)` + +The simplest way to start an Atom build: + +```csharp +AtomHost.Run(args); +``` + +This is what `[GenerateEntryPoint]` generates for you in `Program.cs`. + +## `AtomHost.CreateAtomBuilder(args)` + +For custom host configuration, use the builder pattern: + +```csharp +var builder = AtomHost.CreateAtomBuilder(args); + +// Add your own services +builder.Services.AddSingleton(); + +// Add configuration sources +builder.Configuration.AddJsonFile("custom.json", optional: true); + +builder.Build().UseAtom().Run(); +``` + +The builder automatically: + +- Loads `appsettings.json` and `appsettings.{env}.json` +- Reads the `DOTNET_ENVIRONMENT` / `ASPNETCORE_ENVIRONMENT` variable +- Registers all core Atom services + +## `[GenerateEntryPoint]` + +Apply this attribute to your build definition class to have the source generator create the entry point: + +```csharp +[BuildDefinition] +[GenerateEntryPoint] +internal partial class Build : BuildDefinition { /* ... */ } +``` + +If you need full control over `Main`, omit this attribute and write your own. + +## `[ConfigureHostBuilder]` / `[ConfigureHost]` + +Module interfaces can use `[ConfigureHostBuilder]` to have the source generator call a static `ConfigureBuilder` method +when the interface is implemented: + +```csharp +[ConfigureHostBuilder] +public partial interface IGitVersion +{ + protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) => + builder.Services + .AddSingleton() + .AddSingleton(); +} +``` + +When your `Build` class implements `IGitVersion`, the generated code automatically calls `ConfigureBuilder` during host +setup. + +## `ConfigureDefinitionHost` + +Override this virtual method on your build class for one-off service registration: + +```csharp +public override void ConfigureDefinitionHost(IHostApplicationBuilder builder) +{ + base.ConfigureDefinitionHost(builder); + builder.Services.AddSingleton(); +} +``` + +## Next Steps + +→ [Lifecycle Hooks](lifecycle-hooks.md) + diff --git a/docs/core-concepts/lifecycle-hooks.md b/docs/core-concepts/lifecycle-hooks.md new file mode 100644 index 00000000..67c53ce5 --- /dev/null +++ b/docs/core-concepts/lifecycle-hooks.md @@ -0,0 +1,51 @@ +# Lifecycle Hooks + +`IAtomLifecycleHook` lets you run code at specific points in the Atom build lifecycle without modifying target +definitions. + +## Interface + +```csharp +public interface IAtomLifecycleHook +{ + Task BeforeExecute(CancellationToken cancellationToken) => Task.CompletedTask; + Task AfterExecute(CancellationToken cancellationToken) => Task.CompletedTask; +} +``` + +Both methods have default no-op implementations, so you only override what you need. + +## Hook Points + +| Method | When it runs | Use cases | +|-----------------|-----------------------------------------------------------|--------------------------------------------------| +| `BeforeExecute` | After the build model is resolved, before targets execute | Validation, file generation, precondition checks | +| `AfterExecute` | After all targets complete (success or failure) | Cleanup, reporting, notifications | + +## Registering a Hook + +```csharp +public class MyHook : IAtomLifecycleHook +{ + public Task BeforeExecute(CancellationToken cancellationToken) + { + // e.g. verify workflow files are up to date + return Task.CompletedTask; + } +} + +// In your build definition or module: +builder.Services.AddSingleton(); +``` + +Multiple hooks can be registered. They run in registration order. + +## Built-in Hooks + +The `WorkflowLifecycleHook` (registered by `WorkflowBuildDefinition`) uses `BeforeExecute` to check whether generated +workflow files are outdated and warns or fails accordingly. + +## Next Steps + +→ [Logging & Reports](logging-and-reports.md) + diff --git a/docs/core-concepts/logging-and-reports.md b/docs/core-concepts/logging-and-reports.md new file mode 100644 index 00000000..e15342d8 --- /dev/null +++ b/docs/core-concepts/logging-and-reports.md @@ -0,0 +1,71 @@ +# Logging & Reports + +Atom provides structured logging via [Spectre.Console](https://spectreconsole.net/) and a report system for capturing +build summary data. + +## Logging + +Every target has access to a scoped `ILogger` via `IBuildAccessor.Logger`: + +```csharp +Logger.LogInformation("Building {Project}", projectName); +Logger.LogWarning("Deprecated API detected"); +Logger.LogError("Compilation failed"); +``` + +### Console Output + +Atom uses Spectre.Console for rich terminal rendering, including: + +- Coloured log levels +- Build summary tables +- Target execution progress + +### Verbose Mode + +Pass `--verbose` (or `-v`) to increase log verbosity (shows `Debug`-level output). + +### Headless Mode + +Pass `--headless` (or `-hl`) for non-interactive CI environments where prompts and fancy rendering should be disabled. + +### Log Masking + +Values marked as secrets (via `[SecretDefinition]`) are automatically masked in all log output. + +## Reports + +The report system lets targets contribute structured data to the build summary. + +### `IReportsHelper` + +Targets implementing `IReportsHelper` can add report data: + +```csharp +AddReportData(new TextReportData("Build completed successfully") +{ + Title = "Status", +}); + +AddReportData(new ListReportData(["Warning 1", "Warning 2"]) +{ + Title = "Warnings", +}); +``` + +### Report Writers + +Report data is rendered by `IOutcomeReportWriter` implementations: + +| Writer | Description | +|------------------------------------|------------------------------------------| +| `ConsoleOutcomeReportWriter` | Writes to the console (always active) | +| `GithubSummaryOutcomeReportWriter` | Writes to the GitHub Actions job summary | +| `DevopsSummaryOutcomeReportWriter` | Writes to the Azure DevOps build summary | + +Platform-specific writers are registered by their respective modules. + +## Next Steps + +→ [File Transformations](file-transformations.md) + diff --git a/docs/core-concepts/parameters.md b/docs/core-concepts/parameters.md new file mode 100644 index 00000000..d30f9638 --- /dev/null +++ b/docs/core-concepts/parameters.md @@ -0,0 +1,105 @@ +# Parameters + +Parameters allow you to pass configuration values into your build from multiple sources. + +## Defining a Parameter + +Use `[ParamDefinition]` on a property and `GetParam()` to resolve its value: + +```csharp +[ParamDefinition("my-param", "Description shown in help")] +string? MyParam => GetParam(() => MyParam); +``` + +The first argument is the **command-line name** (kebab-case by convention). The property name is used internally for +`RequiresParam`/`UsesParam`. + +### With a Default Value + +```csharp +[ParamDefinition("configuration", "Build configuration")] +string Configuration => GetParam(() => Configuration, "Release"); +``` + +### With a Custom Converter + +```csharp +[ParamDefinition("retry-count", "Number of retries")] +int RetryCount => GetParam(() => RetryCount, 3, s => int.Parse(s!)); +``` + +## Resolution Order + +When `GetParam` is called, Atom resolves the value from the following sources (first match wins): + +1. **Cache** — a previously resolved value for this build run +2. **Command-line arguments** — `--my-param value` +3. **Environment variables** — `MY_PARAM=value` +4. **Configuration** — `appsettings.json` under the `Params` section +5. **Secrets** — registered `ISecretsProvider` implementations + +You can restrict which sources are checked using the `sources` parameter: + +```csharp +[ParamDefinition("api-url", "API endpoint", sources: ParamSource.Configuration | ParamSource.EnvironmentVariables)] +string? ApiUrl => GetParam(() => ApiUrl); +``` + +### `ParamSource` Flags + +| Flag | Source | +|------------------------|-----------------------------------------------| +| `Cache` | Previously resolved value | +| `CommandLineArgs` | `--param-name value` | +| `EnvironmentVariables` | Environment variable matching the param name | +| `Configuration` | `appsettings.json` / `appsettings.{env}.json` | +| `Secrets` | Registered `ISecretsProvider` implementations | +| `All` | All of the above (default) | + +## Requiring Parameters + +Mark a parameter as required for a target so the build fails early if it's missing: + +```csharp +Target Deploy => t => t + .RequiresParam(nameof(ApiKey)) + .Executes(() => { /* ... */ }); +``` + +## Interactive Prompting + +When running with `--interactive` (or `-i`), Atom will prompt the user for any required parameter that hasn't been +provided: + +```shell +dotnet run -- Deploy --interactive +``` + +## Chained Parameters + +A parameter can depend on other parameters: + +```csharp +[ParamDefinition("full-url", "Complete URL", chainedParams: ["base-url", "path"])] +string? FullUrl => GetParam(() => FullUrl); +``` + +## Configuration File + +Parameters can be set in `appsettings.json`: + +```json +{ + "Params": { + "configuration": "Release", + "my-param": "some-value" + } +} +``` + +Environment-specific overrides work via `appsettings.{DOTNET_ENVIRONMENT}.json`. + +## Next Steps + +→ [Secrets](secrets.md) + diff --git a/docs/core-concepts/process-runner.md b/docs/core-concepts/process-runner.md new file mode 100644 index 00000000..0568d69f --- /dev/null +++ b/docs/core-concepts/process-runner.md @@ -0,0 +1,75 @@ +# Process Runner + +`IProcessRunner` provides a structured way to execute external processes with logging, output capture, and error +handling built in. + +> [!NOTE] +> The process runner types (`IProcessRunner`, `ProcessRunOptions`, `ProcessRunResult`) ship in the standalone +> [`Invex.Process`](https://www.nuget.org/packages/Invex.Process) package, maintained in its own repository. You don't +> need to reference it directly — it is pulled in transitively by `Invex.Atom.Build` and surfaced through +> `IBuildAccessor.ProcessRunner`. + +## Basic Usage + +Access the process runner via `IBuildAccessor.ProcessRunner`: + +```csharp +Target Build => t => t + .Executes(async cancellationToken => + { + await ProcessRunner.RunAsync( + new ProcessRunOptions("dotnet", "build --configuration Release"), + cancellationToken); + }); +``` + +## `ProcessRunOptions` + +| Property | Default | Description | +|------------------------|---------------|----------------------------------------------------------| +| `Name` | *(required)* | Executable name or path (`"dotnet"`, `"git"`, etc.) | +| `Args` | *(required)* | Command-line arguments as a string or `string[]` | +| `WorkingDirectory` | `null` | Working directory; inherits current if `null` | +| `InvocationLogLevel` | `Information` | Log level for the invocation line | +| `OutputLogLevel` | `Debug` | Log level for each stdout line | +| `ErrorLogLevel` | `Warning` | Log level for each stderr line | +| `AllowFailedResult` | `false` | If `false`, non-zero exit code throws | +| `EnvironmentVariables` | `{}` | Extra env vars to inject; `null` value removes a var | +| `TransformOutput` | `null` | Per-line transform for stdout; return `null` to suppress | +| `TransformError` | `null` | Per-line transform for stderr; return `null` to suppress | + +### Array-Based Arguments + +```csharp +new ProcessRunOptions("dotnet", ["build", "--configuration", configuration, verbosityFlag]) +``` + +Empty or whitespace-only entries are automatically filtered out, so you can conditionally include arguments without +string concatenation. + +## `ProcessRunResult` + +Both `Run` and `RunAsync` return a `ProcessRunResult` containing: + +- `ExitCode` — the process exit code +- `Output` — captured stdout lines +- `Error` — captured stderr lines + +## Error Handling + +By default, a non-zero exit code throws an exception that fails the enclosing target. Set `AllowFailedResult = true` to +handle failures yourself: + +```csharp +var result = await ProcessRunner.RunAsync( + new ProcessRunOptions("dotnet", "test") { AllowFailedResult = true }, + cancellationToken); + +if (result.ExitCode != 0) + Logger.LogWarning("Tests failed with exit code {Code}", result.ExitCode); +``` + +## Next Steps + +→ [Build Info](build-info.md) + diff --git a/docs/core-concepts/secrets.md b/docs/core-concepts/secrets.md new file mode 100644 index 00000000..b7869198 --- /dev/null +++ b/docs/core-concepts/secrets.md @@ -0,0 +1,61 @@ +# Secrets + +Secrets are parameters that contain sensitive values (API keys, connection strings, passwords). Atom provides +first-class support for masking them in logs and resolving them from secure stores. + +## Defining a Secret + +Use `[SecretDefinition]` instead of `[ParamDefinition]`: + +```csharp +[SecretDefinition("api-key", "The API key for the service")] +string? ApiKey => GetParam(() => ApiKey); +``` + +`[SecretDefinition]` inherits from `[ParamDefinition]` and sets `IsSecret = true`. When a parameter is marked as a +secret: + +- Its value is **masked** in all log output. +- It can be resolved from registered `ISecretsProvider` implementations in addition to the standard sources. + +## `ISecretsProvider` + +Atom uses a chain-of-responsibility pattern for secret resolution. Implement `ISecretsProvider` to integrate with any +secret store: + +```csharp +public interface ISecretsProvider +{ + string? GetSecret(string key); +} +``` + +Register your provider in the DI container. Multiple providers are queried in registration order — return `null` to +delegate to the next provider. + +### Built-in Providers + +| Provider | Package | Description | +|-----------------------------|-----------------------------------|---------------------------------------------------| +| `DotnetUserSecretsProvider` | `Invex.Atom.Build` | Reads from .NET User Secrets (local development). | +| `AzureKeySecretsProvider` | `Invex.Atom.Module.AzureKeyVault` | Reads from Azure Key Vault. | + +### Registering a Custom Provider + +```csharp +public override void ConfigureDefinitionHost(IHostApplicationBuilder builder) +{ + base.ConfigureDefinitionHost(builder); + builder.Services.AddSingleton(); +} +``` + +## .NET User Secrets + +The built-in `DotnetUserSecretsProvider` integrates with `dotnet user-secrets`. This is the recommended approach for +local development — secrets stay out of source control and are resolved automatically. + +## Next Steps + +→ [Artifacts](artifacts.md) + diff --git a/docs/core-concepts/targets.md b/docs/core-concepts/targets.md new file mode 100644 index 00000000..08538c74 --- /dev/null +++ b/docs/core-concepts/targets.md @@ -0,0 +1,134 @@ +# Targets + +A **target** is the fundamental unit of work in an Atom build. Each target has a name, an optional description, +dependencies on other targets, and one or more tasks to execute. + +## Defining a Target + +Targets are declared as properties of type `Target` (a delegate alias for `Func`): + +```csharp +private Target Compile => t => t + .DescribedAs("Compiles the solution") + .Executes(() => + { + // build logic here + }); +``` + +The property name becomes the target name on the command line: `dotnet run -- Compile`. + +## Fluent API + +`TargetDefinition` exposes a fluent API for configuring every aspect of a target: + +### Description & Visibility + +```csharp +t => t + .DescribedAs("Human-readable description for help output") + .IsHidden() // hide from default help; still executable + .WithAlias("c") // short alias for the CLI +``` + +### Execution + +Targets can execute synchronous actions, async tasks, or async tasks with cancellation: + +```csharp +// Synchronous +t => t.Executes(() => Console.WriteLine("done")) + +// Async +t => t.Executes(async () => await DoWorkAsync()) + +// Async with CancellationToken +t => t.Executes(async cancellationToken => await DoWorkAsync(cancellationToken)) +``` + +You can call `.Executes()` multiple times — tasks run in order. + +### Dependencies + +```csharp +t => t + .DependsOn(Restore) // inferred from property expression + .DependsOn(nameof(Compile)) // by name + .DependsOn(Compile, Test) // multiple at once +``` + +Dependencies are resolved transitively. If `Pack` depends on `Compile` and `Compile` depends on `Restore`, running +`Pack` executes `Restore → Compile → Pack`. + +### Parameters + +```csharp +t => t + .RequiresParam(nameof(MyName)) // build fails if not provided + .UsesParam(nameof(Verbosity)) // optional; documented but not enforced +``` + +### Artifacts + +```csharp +t => t + .ProducesArtifact("packages") + .ConsumesArtifact(nameof(Pack), "packages") +``` + +### Variables + +```csharp +t => t + .ProducesVariable("BuildVersion") + .ConsumesVariable(nameof(SetupBuildInfo), "BuildVersion") +``` + +## Extending Targets + +A target can extend another target defined in an interface, inheriting its tasks, dependencies, parameters, artifacts, +and variables: + +```csharp +Target MyCompile => t => t + .Extends(x => x.DotnetBuild) + .Executes(() => Logger.LogInformation("Extra step after build")); +``` + +By default the extending target's tasks run **before** the base target's tasks. Pass `runExtensionAfter: true` to +reverse the order: + +```csharp +.Extends(x => x.DotnetBuild, runExtensionAfter: true) +``` + +## Accessing Services + +Inside a target body you have access to several built-in services via `IBuildAccessor`: + +| Property | Type | Description | +|------------------|--------------------|-----------------------------------------------| +| `Logger` | `ILogger` | Structured logger scoped to the current type. | +| `RootedFileSystem` | `IRootedFileSystem` | File system abstraction with path resolution. | +| `ProcessRunner` | `IProcessRunner` | Execute external processes. | +| `Services` | `IServiceProvider` | Full DI container. | + +Use `GetService()` or `GetServices()` for any other registered service. + +## Running Targets + +```shell +# Single target +dotnet run -- Compile + +# Multiple targets +dotnet run -- Compile Test + +# Skip dependencies +dotnet run -- Pack --skip +``` + +## Next Steps + +→ [Parameters](parameters.md) + diff --git a/docs/core-concepts/variables.md b/docs/core-concepts/variables.md new file mode 100644 index 00000000..c13b4503 --- /dev/null +++ b/docs/core-concepts/variables.md @@ -0,0 +1,61 @@ +# Variables + +Variables allow targets to share simple string values with downstream targets. Unlike parameters (which are inputs), +variables are **outputs** produced during execution. + +## Producing a Variable + +Declare and write a variable: + +```csharp +Target SetupVersion => t => t + .ProducesVariable("BuildVersion") + .Executes(async cancellationToken => + { + var version = "1.2.3"; + await WriteVariable("BuildVersion", version, cancellationToken); + }); +``` + +`WriteVariable` is available via the `IVariablesHelper` interface. + +## Consuming a Variable + +Declare that a target consumes a variable from another target: + +```csharp +Target Pack => t => t + .ConsumesVariable(nameof(SetupVersion), "BuildVersion") + .Executes(async cancellationToken => + { + var version = await ReadVariable(nameof(SetupVersion), "BuildVersion", cancellationToken); + Logger.LogInformation("Packing version {Version}", version); + }); +``` + +## How Variables Work + +- **Locally**, variables are stored in-memory and shared between targets in the same process. +- **In workflows**, variables are mapped to the platform's native mechanism (e.g. GitHub Actions job outputs, Azure + DevOps pipeline variables) so they can cross job boundaries. + +## `IVariableProvider` + +The framework uses `IVariableProvider` implementations to read and write variables. Multiple providers form a chain of +responsibility: + +```csharp +public interface IVariableProvider +{ + Task WriteVariable(string variableName, string variableValue, CancellationToken ct); + Task ReadVariable(string jobName, string variableName, CancellationToken ct); +} +``` + +The built-in `AtomVariableProvider` handles local execution. Platform modules (GitHub, DevOps) register their own +providers for CI environments. + +## Next Steps + +→ [File System](file-system.md) + diff --git a/docs/developer-guide/custom-providers.md b/docs/developer-guide/custom-providers.md new file mode 100644 index 00000000..abe09332 --- /dev/null +++ b/docs/developer-guide/custom-providers.md @@ -0,0 +1,123 @@ +# Custom Providers + +Atom uses a provider pattern for several extensibility points. Implement these interfaces to integrate with custom +backends. + +## `ISecretsProvider` + +Retrieve secrets from a custom store: + +```csharp +public class MySecretsProvider : ISecretsProvider +{ + public string? GetSecret(string key) + { + // Return the secret value, or null to delegate to the next provider. + return MyVault.TryGet(key); + } +} +``` + +Register it: + +```csharp +builder.Services.AddSingleton(); +``` + +Multiple providers form a chain — return `null` to pass through. + +## `IArtifactProvider` + +Store and retrieve build artifacts in a custom location: + +```csharp +public class MyArtifactProvider : IArtifactProvider +{ + public IReadOnlyList RequiredParams => ["my-storage-url"]; + + public Task StoreArtifacts(IEnumerable artifactNames, string? buildId, string? buildSlice, CancellationToken ct) + { + // Upload artifacts + } + + public Task RetrieveArtifacts(IEnumerable artifactNames, string? buildId, string? buildSlice, CancellationToken ct) + { + // Download artifacts + } + + public Task Cleanup(IEnumerable runIdentifiers, CancellationToken ct) + { + // Remove old artifacts + } + + public Task> GetStoredRunIdentifiers(string? artifactName, string? buildSlice, CancellationToken ct) + { + // List known runs + } +} +``` + +Register as a singleton — only one `IArtifactProvider` is active at a time. + +## `IVariableProvider` + +Read and write workflow variables via a custom mechanism: + +```csharp +public class MyVariableProvider : IVariableProvider +{ + public Task WriteVariable(string variableName, string variableValue, CancellationToken ct) + { + // Return true if handled, false to delegate + } + + public Task ReadVariable(string jobName, string variableName, CancellationToken ct) + { + // Return true if found, false to delegate + } +} +``` + +Multiple providers form a chain. + +## `IBuildIdProvider` / `IBuildVersionProvider` / `IBuildTimestampProvider` + +Replace how the build identity is determined: + +```csharp +public class MyVersionProvider : IBuildVersionProvider +{ + public string BuildVersion => ReadVersionFromSomewhere(); +} +``` + +## `IPathProvider` + +Add custom well-known paths: + +```csharp +public class MyPathProvider : IPathProvider +{ + public RootedPath? GetPath(string key) => key switch + { + "MyCustomPath" => new RootedPath(fileSystem, "/custom/path"), + _ => null, + }; +} +``` + +## `IOutcomeReportWriter` + +Write build reports to a custom destination: + +```csharp +public class MyReportWriter : IOutcomeReportWriter +{ + // Write report data to Slack, email, a database, etc. +} +``` + +## Next Steps + +→ [Source Generators](source-generators.md) + diff --git a/docs/developer-guide/source-generators.md b/docs/developer-guide/source-generators.md new file mode 100644 index 00000000..7420659b --- /dev/null +++ b/docs/developer-guide/source-generators.md @@ -0,0 +1,72 @@ +# Source Generators + +Atom uses Roslyn source generators and analysers to reduce boilerplate. This page explains what they do and how they +affect your build definition. + +## Packages + +| Package | Role | +|-------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Invex.Atom.Build.SourceGenerators` | Generates code for `[BuildDefinition]`, `[GenerateEntryPoint]`, `[ConfigureHostBuilder]`, `[GenerateSolutionModel]`, and `[GenerateInterfaceMembers]`. | +| `Invex.Atom.Build.Analyzers` | Reports diagnostics for common mistakes (e.g. forgetting `partial`, missing attributes). | + +Both are automatically referenced when you add `Invex.Atom.Build`. + +## What Gets Generated + +### `[BuildDefinition]` + +For a class decorated with `[BuildDefinition]`, the generator emits: + +- `TargetDefinitions` — a dictionary mapping target names to `Target` delegates, collected from the class and all + implemented interfaces. +- `ParamDefinitions` — a dictionary mapping parameter names to `ParamDefinition` records, collected from + `[ParamDefinition]` / `[SecretDefinition]` attributes. +- `AccessParam` — a method that can read any declared parameter by name. + +### `[GenerateEntryPoint]` + +Emits a `Program` class with: + +```csharp +AtomHost.Run(args); +``` + +### `[ConfigureHostBuilder]` + +For interfaces marked with `[ConfigureHostBuilder]`, the generator ensures that when a `[BuildDefinition]` class +implements the interface, the static `ConfigureBuilder` method is called during host setup. + +### `[GenerateSolutionModel]` + +Scans the solution file and emits a typed model with project paths. + +### `[GenerateInterfaceMembers]` + +Generates boilerplate interface members (e.g. forwarding properties) so module authors don't have to write them +manually. + +## Analyser Diagnostics + +The analyser package reports warnings and errors such as: + +- Build definition class is not `partial` +- `[BuildDefinition]` is missing on a class that inherits `BuildDefinition` +- Target properties that don't follow the expected pattern + +## Debugging Generators + +To inspect the generated code: + +1. In your `.csproj`, add: + + ```xml + true + ``` + +2. Build the project. Generated files appear under `obj/Debug/net*/generated/`. + +## Next Steps + +→ [Testing](testing.md) + diff --git a/docs/developer-guide/testing.md b/docs/developer-guide/testing.md new file mode 100644 index 00000000..a7ee7942 --- /dev/null +++ b/docs/developer-guide/testing.md @@ -0,0 +1,46 @@ +# Testing + +**Package:** `Invex.Atom.TestUtils` + +Atom provides test utilities for verifying your build definitions, targets, and modules in unit/integration tests. + +## Installation + +```shell +dotnet add package Invex.Atom.TestUtils +``` + +## Usage + +`Invex.Atom.TestUtils` provides helpers for: + +- Creating test build definitions +- Mocking services (file system, process runner, etc.) +- Verifying target execution order +- Testing parameter resolution +- Validating generated workflow models + +## Testing a Target + +Set up a test build with mocked services, execute a target, and assert the results: + +```csharp +// Arrange - create a test host with your build definition +// Act - execute the target +// Assert - verify the expected behaviour +``` + +## Testing Workflow Generation + +Verify that your workflow definitions produce the expected YAML by: + +1. Instantiating the build definition in a test context. +2. Running the workflow resolver. +3. Comparing the output model against expected values. + +## Tips + +- Use `System.IO.Abstractions.TestingHelpers` (already a dependency) for in-memory file system testing. +- Mock `IProcessRunner` to avoid executing real processes in tests. +- Use snapshot testing to verify generated YAML doesn't change unexpectedly. + diff --git a/docs/developer-guide/writing-a-module.md b/docs/developer-guide/writing-a-module.md new file mode 100644 index 00000000..c449586a --- /dev/null +++ b/docs/developer-guide/writing-a-module.md @@ -0,0 +1,104 @@ +# Writing a Module + +This guide covers how to create a reusable Atom module and publish it as a NuGet package. + +## What Is a Module? + +A module is a NuGet package that provides one or more of: + +- **Targets** — reusable build steps +- **Parameters / Secrets** — configuration the module needs +- **Service registrations** — DI services (providers, helpers) +- **Build options** — workflow-level configuration + +## Project Setup + +1. Create a class library: + + ```shell + dotnet new classlib -n Invex.Atom.Module.MyModule + ``` + +2. Add a reference to `Invex.Atom.Build` (and `Invex.Atom.Workflows` if your module contributes workflow features): + + ```xml + + ``` + +3. Create a `.props` file (optional but recommended) to auto-import usings when consumers reference your package. + +## Define the Module Interface + +Modules expose their functionality through interfaces that extend `IBuildAccessor`: + +```csharp +[PublicAPI] +[ConfigureHostBuilder] +public partial interface IMyModule : IBuildAccessor +{ + [ParamDefinition("my-setting", "A setting for my module")] + string? MySetting => GetParam(() => MySetting); + + Target MyTarget => t => t + .DescribedAs("Does something useful") + .RequiresParam(nameof(MySetting)) + .Executes(() => + { + Logger.LogInformation("Setting: {Setting}", MySetting); + }); + + protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) => + builder.Services.AddSingleton(); +} +``` + +### Key Points + +- **`[ConfigureHostBuilder]`** + `static partial void ConfigureBuilder` — the source generator calls this method when a + consumer implements the interface. This is how you register services. +- **`IBuildAccessor`** — gives the interface access to `Logger`, `RootedFileSystem`, `ProcessRunner`, `GetParam`, etc. +- **Default implementations** — both targets and parameters use default interface members, so consumers don't need to + implement anything. + +## Extend Build Options (Optional) + +If your module needs workflow-level options, extend the static `BuildOptions` class: + +```csharp +public static class BuildOptions +{ + public static class MyModule + { + public static IBuildOption UseSomething => new ToggleBuildOption("MyModule.UseSomething"); + } +} +``` + +## Package and Publish + +1. Set the appropriate NuGet metadata in your `.csproj`. +2. Pack: `dotnet pack` +3. Publish to NuGet or a private feed. + +## Consumer Usage + +```csharp +[BuildDefinition] +[GenerateEntryPoint] +internal partial class Build : BuildDefinition, IMyModule { } +``` + +The consumer just implements the interface — all targets, parameters, and services appear automatically. + +## Conventions + +- Name your package `Invex.Atom.Module.` (or your own prefix). +- Prefix parameter CLI names to avoid collisions (e.g. `mymodule-setting`). +- Mark the interface with `[PublicAPI]` for API documentation. +- Provide XML doc comments on all public members. +- Include a `.props` file to auto-import common namespaces. + +## Next Steps + +→ [Custom Providers](custom-providers.md) + diff --git a/docs/filterConfig.yml b/docs/filterConfig.yml new file mode 100644 index 00000000..55bfb8e7 --- /dev/null +++ b/docs/filterConfig.yml @@ -0,0 +1,12 @@ +# Filter out internal types and members from API docs +apiRules: + - exclude: + hasAttribute: + uid: System.Runtime.CompilerServices.CompilerGeneratedAttribute + - exclude: + uidRegex: ^.*\.Internal\..*$ + - include: + uidRegex: ^DecSm\. + - exclude: + uidRegex: ^(?!DecSm\.) + diff --git a/docs/getting-started/base-vs-workflow-build.md b/docs/getting-started/base-vs-workflow-build.md new file mode 100644 index 00000000..dceb161f --- /dev/null +++ b/docs/getting-started/base-vs-workflow-build.md @@ -0,0 +1,88 @@ +# Base Build vs Workflow Build + +Atom offers two base classes for your build definition. Which one you choose depends on whether you need to generate +CI/CD pipeline files. + +## `BuildDefinition` — The Base Build + +Use `BuildDefinition` when you only need to run builds locally (or you manage your CI YAML by hand). + +```csharp +[BuildDefinition] +[GenerateEntryPoint] +internal partial class Build : BuildDefinition +{ + private Target Compile => t => t + .DescribedAs("Compiles the solution") + .Executes(() => { /* ... */ }); +} +``` + +`BuildDefinition` gives you: + +- Target discovery and execution with dependency resolution +- Parameter and secret management +- Artifact and variable support +- Process runner, file system, logging, reports +- All core concepts documented in this guide + +## `WorkflowBuildDefinition` — Adding CI/CD Generation + +`WorkflowBuildDefinition` extends `BuildDefinition` with the ability to define **workflows** — descriptions of how your +targets map to CI/CD jobs — and generate the corresponding YAML files. + +```csharp +[BuildDefinition] +[GenerateEntryPoint] +internal partial class Build : WorkflowBuildDefinition +{ + private Target Compile => t => t + .DescribedAs("Compiles the solution") + .Executes(() => { /* ... */ }); + + private Target Test => t => t + .DescribedAs("Runs tests") + .DependsOn(Compile) + .Executes(() => { /* ... */ }); + + public override IReadOnlyList Workflows => + [ + new("CI") + { + Triggers = [WorkflowTriggers.PushToMain, WorkflowTriggers.PullIntoMain], + Targets = + [ + new(nameof(Compile)), + new(nameof(Test)), + ], + Types = [WorkflowTypes.Github.Action], + }, + ]; +} +``` + +Running `dotnet run -- GenerateWorkflowFiles` (or `dotnet run -- Gen`) writes a GitHub Actions YAML file that calls your +build with the correct targets. + +### What `WorkflowBuildDefinition` adds + +| Feature | Description | +|--------------------------------|-----------------------------------------------------------------------------------------------------------| +| `Workflows` property | Declare named workflows with triggers, targets, and platform types. | +| `GenerateWorkflowFiles` target | Generates YAML for each workflow. | +| Workflow triggers | Push, pull request, manual (with inputs). | +| Matrix dimensions | Run a target across multiple OS or framework combinations. | +| Workflow options | Checkout steps, deployment environments, run conditions, etc. | +| Platform modules | `Invex.Atom.Module.GithubWorkflows` / `Invex.Atom.Module.DevopsWorkflows` add platform-specific features. | + +### When to upgrade + +You can always start with `BuildDefinition` and switch to `WorkflowBuildDefinition` later — the change is additive. Your +existing targets, parameters, and modules continue to work unchanged; you just gain the `Workflows` property and the +`GenerateWorkflowFiles` target. + +## Next Steps + +→ [Build Definitions](../core-concepts/build-definitions.md) — deep dive into the `[BuildDefinition]` attribute and +source generators + diff --git a/docs/getting-started/introduction.md b/docs/getting-started/introduction.md new file mode 100644 index 00000000..dcfd9d74 --- /dev/null +++ b/docs/getting-started/introduction.md @@ -0,0 +1,55 @@ +# Introduction + +Atom is a build automation framework for .NET that lets you define your entire build pipeline in C#. Instead of +maintaining separate YAML or script files, you write strongly-typed build logic alongside your application code, gaining +full IDE support — IntelliSense, refactoring, and step-through debugging. + +## Key Concepts + +| Concept | Description | +|----------------------|------------------------------------------------------------------------------------------------------------------------| +| **Build Definition** | A C# class that declares your targets, parameters, and build configuration. | +| **Target** | A named unit of work (compile, test, pack, deploy, etc.) with optional dependencies on other targets. | +| **Parameter** | A value that can be supplied via the command line, environment variable, `appsettings.json`, or a secrets provider. | +| **Module** | A NuGet package that adds reusable targets, parameters, or service registrations to your build. | +| **Workflow** | An optional layer that maps targets to CI/CD jobs and generates platform-specific YAML (GitHub Actions, Azure DevOps). | + +## Package Overview + +Atom is split into several NuGet packages so you only pull in what you need: + +| Package | Purpose | +|------------------------|-----------------------------------------------------------------------| +| `Invex.Atom.Build` | Core framework — build definitions, targets, parameters, hosting. | +| `Invex.Atom.Workflows` | Workflow definitions, triggers, and YAML generation. | +| `Invex.Atom.Module.*` | First-party modules (Dotnet, GitVersion, AzureKeyVault, etc.). | +| `Invex.Atom.Tool` | The `atom` .NET global tool for running builds from the command line. | + +### Foundational Libraries + +Several lower-level building blocks live in their own repositories and are published as standalone packages. You don't +normally reference these directly — they are pulled in transitively by `Invex.Atom.Build` — but their types surface +through Atom (for example via `IBuildAccessor.RootedFileSystem` and `IBuildAccessor.ProcessRunner`): + +| Package | Purpose | +|-------------------------|------------------------------------------------------------------------------| +| `Invex.FileSystem` | `IRootedFileSystem`, `RootedPath`, path providers, and file transformations. | +| `Invex.Process` | `IProcessRunner` for executing external tools. | +| `Invex.SemanticVersion` | Semantic versioning utilities. | +| `Invex.StructuredText` | Structured text / YAML writing used by the workflow generators. | + +## How It Works + +1. You create a C# project (or a single `.cs` file) that references the Atom packages. +2. You define a class decorated with `[BuildDefinition]` that inherits from `BuildDefinition` (or + `WorkflowBuildDefinition` if you need CI/CD generation). +3. Inside that class you declare **targets** — lambda-based definitions that describe what to execute, their + dependencies, required parameters, and produced artifacts. +4. You run the build with `dotnet run -- ` (or via the `atom` global tool). +5. If you use `WorkflowBuildDefinition`, running the `GenerateWorkflowFiles` target emits platform-specific YAML that + invokes your same build on CI. + +## Next Steps + +→ [Your First Build](your-first-build.md) + diff --git a/docs/getting-started/your-first-build.md b/docs/getting-started/your-first-build.md new file mode 100644 index 00000000..b5518195 --- /dev/null +++ b/docs/getting-started/your-first-build.md @@ -0,0 +1,142 @@ +# Your First Build + +This guide walks you through creating a minimal Atom build and running it locally. + +## Prerequisites + +- [.NET 8 SDK](https://dotnet.microsoft.com/download) or later + +## Option 1 — Single-File Build (Simplest) + +Create a file called `Build.cs` anywhere on disk: + +```csharp +#:package Invex.Atom@2.* + +[BuildDefinition] +[GenerateEntryPoint] +partial class Build : BuildDefinition +{ + Target SayHello => t => t + .DescribedAs("Prints a hello world message") + .Executes(() => Logger.LogInformation("Hello, World!")); +} +``` + +Run it: + +```shell +dotnet run Build.cs SayHello +``` + +That's it. The `#:package` directive pulls in the Atom NuGet package automatically, `[GenerateEntryPoint]` +source-generates a `Main` method, and the `SayHello` target is discovered and executed. + +## Option 2 — Project-Based Build + +For larger builds you'll typically use a dedicated project. + +1. Create a new console project: + + ```shell + dotnet new console -n _atom + ``` + +2. Add the Atom package: + + ```shell + cd _atom + dotnet add package Invex.Atom + ``` + +3. Replace `Program.cs` with a build definition (or use `[GenerateEntryPoint]` to have the entry point generated for + you). Here's the minimal version with `[GenerateEntryPoint]`: + + ```csharp + using Invex.Atom.Build.Definition; + using Invex.Atom.Build.Hosting; + + namespace Atom; + + [BuildDefinition] + [GenerateEntryPoint] + internal partial class Build : BuildDefinition + { + private Target HelloWorld => t => t + .DescribedAs("Prints a hello world message") + .Executes(() => + { + Logger.LogInformation("Hello, World!"); + }); + } + ``` + +4. Run the build: + + ```shell + dotnet run -- HelloWorld + ``` + +### Expected Output + +``` +25-12-16 +10:00 Invex.Atom.Build.BuildExecutor: +22:46:01.754 INF Executing build + +HelloWorld + +25-12-16 +10:00 HelloWorld | Build: +22:46:01.790 INF Hello, World! + +Build Summary + + HelloWorld │ Succeeded │ <0.01s +``` + +## Adding Parameters + +Parameters let you pass values into targets from the command line, configuration, or environment variables. + +```csharp +[BuildDefinition] +[GenerateEntryPoint] +internal partial class Build : BuildDefinition +{ + [ParamDefinition("my-name", "Name to greet")] + private string? MyName => GetParam(() => MyName); + + private Target Hello => t => t + .DescribedAs("Prints a greeting") + .RequiresParam(nameof(MyName)) + .Executes(() => Logger.LogInformation("Hello, {Name}!", MyName)); +} +``` + +Run with a parameter: + +```shell +dotnet run -- Hello --my-name World +``` + +Or interactively: + +```shell +dotnet run -- Hello --interactive +``` + +Atom will prompt you for any required parameters that haven't been provided. + +Parameters can also be supplied via `appsettings.json`: + +```json +{ + "Params": { + "my-name": "World" + } +} +``` + +## Next Steps + +→ [Base vs Workflow Build](base-vs-workflow-build.md) — understand when you need workflow support + diff --git a/docs/modules/azure-key-vault.md b/docs/modules/azure-key-vault.md new file mode 100644 index 00000000..496bf0be --- /dev/null +++ b/docs/modules/azure-key-vault.md @@ -0,0 +1,67 @@ +# Module: Azure Key Vault + +**Package:** `Invex.Atom.Module.AzureKeyVault` + +Integrates Azure Key Vault as a secrets provider, allowing your build to resolve `[SecretDefinition]` parameters from a +vault. + +## Installation + +```shell +dotnet add package Invex.Atom.Module.AzureKeyVault +``` + +## Usage + +```csharp +[BuildDefinition] +[GenerateEntryPoint] +internal partial class Build : BuildDefinition, IAzureKeyVault +{ + // All IAzureKeyVault parameters are now available. +} +``` + +## Configuration Parameters + +| Parameter | CLI Arg | Description | +|-----------------------|----------------------------|------------------------------------------| +| `AzureVaultAddress` | `--azure-vault-address` | URI of the Azure Key Vault | +| `AzureVaultTenantId` | `--azure-vault-tenant-id` | Azure AD Tenant ID | +| `AzureVaultAppId` | `--azure-vault-app-id` | App Registration Client ID | +| `AzureVaultAppSecret` | `--azure-vault-app-secret` | App Registration Client Secret | +| `AzureVaultAuthPort` | `--azure-vault-auth-port` | Local auth redirect port (default: 3421) | + +These parameters can be provided via environment variables, `appsettings.json`, or the command line. + +## How It Works + +When `IAzureKeyVault` is implemented, the module registers: + +- `AzureKeySecretsProvider` as an `ISecretsProvider` — queries the vault for secret values. +- `AzureKeyOptionsProvider` as an `IBuildOptionProvider` — contributes workflow options for injecting vault credentials + in CI. + +## Value Injection Customisation + +Control how each vault parameter is injected in workflows: + +```csharp +AzureKeyVaultValueInjections AzureKeyVaultValueInjections => new( + Address: AzureKeyVaultValueInjectionType.EnvironmentVariable, + TenantId: AzureKeyVaultValueInjectionType.EnvironmentVariable, + AppId: AzureKeyVaultValueInjectionType.EnvironmentVariable, + AppSecret: AzureKeyVaultValueInjectionType.Secret); +``` + +## Workflow Option + +Enable vault integration at the build level: + +```csharp +public override IReadOnlyList Options => +[ + BuildOptions.AzureKeyVault.UseAzureKeyVault, +]; +``` + diff --git a/docs/modules/azure-storage.md b/docs/modules/azure-storage.md new file mode 100644 index 00000000..d1f5b782 --- /dev/null +++ b/docs/modules/azure-storage.md @@ -0,0 +1,42 @@ +# Module: Azure Storage + +**Package:** `Invex.Atom.Module.AzureStorage` + +Registers Azure Blob Storage as the artifact provider, enabling artifact upload and download via Azure Storage +containers. + +## Installation + +```shell +dotnet add package Invex.Atom.Module.AzureStorage +``` + +## Usage + +```csharp +[BuildDefinition] +[GenerateEntryPoint] +internal partial class Build : BuildDefinition, IAzureArtifactStorage +{ + // Artifact targets now use Azure Blob Storage. +} +``` + +`IAzureArtifactStorage` extends both `IStoreArtifact` and `IRetrieveArtifact`, so you get both upload and download +targets. + +## Configuration Parameters + +| Parameter | CLI Arg | Type | Description | +|----------------------------------------|--------------------------------------------|--------|---------------------------------| +| `AzureArtifactStorageConnectionString` | `--azurestorage-artifact-connectionstring` | Secret | Azure Storage connection string | +| `AzureArtifactStorageContainer` | `--azurestorage-artifact-container` | Param | Container name | + +## How It Works + +The module registers `AzureBlobArtifactProvider` as the singleton `IArtifactProvider`. All artifact operations (store, +retrieve, cleanup, list) go through Azure Blob Storage instead of the local file system. + +This is particularly useful in CI/CD workflows where artifacts need to be shared across jobs running on different +machines. + diff --git a/docs/modules/devops-workflows.md b/docs/modules/devops-workflows.md new file mode 100644 index 00000000..fd162639 --- /dev/null +++ b/docs/modules/devops-workflows.md @@ -0,0 +1,72 @@ +# Module: DevOps Workflows + +**Package:** `Invex.Atom.Module.DevopsWorkflows` + +Adds Azure DevOps Pipelines support to Atom — pipeline YAML generation, DevOps-specific options, and access to Azure +DevOps environment variables. + +## Installation + +```shell +dotnet add package Invex.Atom.Module.DevopsWorkflows +``` + +## Usage + +```csharp +[BuildDefinition] +[GenerateEntryPoint] +internal partial class Build : WorkflowBuildDefinition, IDevopsWorkflows +{ + public override IReadOnlyList Workflows => + [ + new("Build") + { + Triggers = [WorkflowTriggers.PushToMain], + Targets = [new(nameof(Compile))], + Types = [WorkflowTypes.Devops.Pipeline], + }, + ]; +} +``` + +## Features + +### Pipeline Generation + +Generates Azure DevOps Pipeline YAML files. + +### `Devops` Static Class + +Access Azure DevOps environment variables: + +```csharp +if (Devops.IsDevopsPipelines) +{ + var buildId = Devops.Variables.BuildBuildId; + var branch = Devops.Variables.BuildSourceBranch; +} +``` + +### Variable Provider + +`DevopsVariableProvider` maps Atom variables to Azure DevOps pipeline variables. + +### Report Writer + +`DevopsSummaryOutcomeReportWriter` writes build report data to the Azure DevOps build summary. + +### Pool Labels + +```csharp +WorkflowLabels.Devops.Pool.Ubuntu_Latest +WorkflowLabels.Devops.Pool.Windows_Latest +WorkflowLabels.Devops.Pool.MacOs_Latest +``` + +### DevOps-Specific Options + +- `BuildOptions.Devops.DevopsPool.SetByMatrix` — set the agent pool from a matrix dimension +- `BuildOptions.Devops.VariableGroup.*` — reference Azure DevOps variable groups +- `ProvideDevopsRunIdAsWorkflowId` — use the DevOps run ID as the Atom build ID + diff --git a/docs/modules/dotnet.md b/docs/modules/dotnet.md new file mode 100644 index 00000000..c5fb6442 --- /dev/null +++ b/docs/modules/dotnet.md @@ -0,0 +1,46 @@ +# Module: .NET + +**Package:** `Invex.Atom.Module.Dotnet` + +Provides helpers and targets for common .NET CLI operations — build, test, pack, publish, and NuGet push. + +## Installation + +```shell +dotnet add package Invex.Atom.Module.Dotnet +``` + +## Usage + +The module exposes several interfaces with pre-built targets for .NET CLI commands. Implement the relevant interfaces on +your build class to gain access to the targets and their parameters. + +## Features + +- **CLI wrappers**: Strongly-typed wrappers around `dotnet build`, `dotnet test`, `dotnet pack`, `dotnet publish`, and + `dotnet nuget push`. +- **Build helpers**: Utility methods for common .NET build patterns. +- **Framework labels**: `WorkflowLabels.Dotnet.Framework.*` constants for matrix dimensions (`Net_8_0`, `Net_9_0`, + `Net_10_0`, etc.). + +## Workflow Integration + +Use the framework labels for matrix builds: + +```csharp +new MatrixDimension(nameof(ITestTargets.TestFramework)) +{ + Values = [ + WorkflowLabels.Dotnet.Framework.Net_8_0, + WorkflowLabels.Dotnet.Framework.Net_9_0, + ], +} +``` + +Setup steps ensure the required .NET SDK versions are installed on CI runners: + +```csharp +BuildOptions.Steps.SetupDotnet.Dotnet80X() +BuildOptions.Steps.SetupDotnet.Dotnet90X() +``` + diff --git a/docs/modules/git-version.md b/docs/modules/git-version.md new file mode 100644 index 00000000..87a27331 --- /dev/null +++ b/docs/modules/git-version.md @@ -0,0 +1,56 @@ +# Module: GitVersion + +**Package:** `Invex.Atom.Module.GitVersion` + +Integrates [GitVersion](https://gitversion.net/) to provide automatic build ID and version numbers based on your Git +history. + +## Installation + +```shell +dotnet add package Invex.Atom.Module.GitVersion +``` + +## Usage + +```csharp +[BuildDefinition] +[GenerateEntryPoint] +internal partial class Build : BuildDefinition, IGitVersion +{ + // BuildId and BuildVersion are now sourced from GitVersion. +} +``` + +## What It Does + +When `IGitVersion` is implemented, the module registers: + +- `GitVersionBuildIdProvider` as `IBuildIdProvider` +- `GitVersionBuildVersionProvider` as `IBuildVersionProvider` + +These replace the default providers, so `BuildId` and `BuildVersion` in your `IBuildInfo` are automatically populated +from GitVersion output. + +## Workflow Options + +Enable at the build level: + +```csharp +public override IReadOnlyList Options => +[ + BuildOptions.GitVersion.ProvideBuildId, + BuildOptions.GitVersion.ProvideBuildVersion, +]; +``` + +## Prerequisites + +GitVersion must be available as a .NET tool. Install it globally or as a local tool: + +```shell +dotnet tool install --global GitVersion.Tool +``` + +Configure it via `GitVersion.yml` in your repository root. + diff --git a/docs/modules/github-workflows.md b/docs/modules/github-workflows.md new file mode 100644 index 00000000..febe54c7 --- /dev/null +++ b/docs/modules/github-workflows.md @@ -0,0 +1,96 @@ +# Module: GitHub Workflows + +**Package:** `Invex.Atom.Module.GithubWorkflows` + +Adds GitHub Actions support to Atom — workflow YAML generation, GitHub-specific build options, variable handling, and +access to GitHub environment variables. + +## Installation + +```shell +dotnet add package Invex.Atom.Module.GithubWorkflows +``` + +## Usage + +```csharp +[BuildDefinition] +[GenerateEntryPoint] +internal partial class Build : WorkflowBuildDefinition, IGithubWorkflows +{ + public override IReadOnlyList Workflows => + [ + new("CI") + { + Triggers = [WorkflowTriggers.PushToMain], + Targets = [new(nameof(Compile))], + Types = [WorkflowTypes.Github.Action], + }, + ]; +} +``` + +Running `dotnet run -- Gen` generates `.github/workflows/CI.yml`. + +## Features + +### Workflow Generation + +The module provides a `WorkflowFileWriter` that generates GitHub Actions YAML files in `.github/workflows/`. + +### `Github` Static Class + +Access all GitHub Actions environment variables in a typed way: + +```csharp +if (Github.IsGithubActions) +{ + var sha = Github.Variables.Sha; + var ref = Github.Variables.Ref; +} +``` + +### Variable Provider + +`GithubVariableProvider` writes variables to `$GITHUB_OUTPUT` and reads them from job outputs, enabling cross-job data +sharing. + +### Report Writer + +`GithubSummaryOutcomeReportWriter` writes build report data to the GitHub Actions job summary (`$GITHUB_STEP_SUMMARY`). + +### Runner Labels + +Use `WorkflowLabels.Github.RunsOn.*` constants for runner selection: + +```csharp +WorkflowLabels.Github.RunsOn.Ubuntu_Latest +WorkflowLabels.Github.RunsOn.Windows_Latest +WorkflowLabels.Github.RunsOn.MacOs_Latest +``` + +### GitHub-Specific Options + +- `BuildOptions.Github.RunsOn.SetByMatrix` — set the runner from a matrix dimension +- `GithubTokenPermissionsOption` — fine-grained `GITHUB_TOKEN` permissions +- GitHub-specific triggers (e.g. `release` events) + +### Dependabot Configuration + +Generate Dependabot configuration alongside your workflow files: + +```csharp +WorkflowPresets.Github.Dependabot(new() +{ + Updates = + [ + new() + { + Directory = "/", + PackageEcosystem = WorkflowLabels.Github.Dependabot.NugetEcosystem, + Schedule = new() { Interval = ScheduleInterval.Daily }, + }, + ], +}) +``` + diff --git a/docs/modules/overview.md b/docs/modules/overview.md new file mode 100644 index 00000000..bd129a53 --- /dev/null +++ b/docs/modules/overview.md @@ -0,0 +1,61 @@ +# Modules Overview + +A **module** is a NuGet package that adds reusable functionality to an Atom build — targets, parameters, service +registrations, or workflow options. + +## How Modules Work + +Modules are typically distributed as interfaces. Your build definition implements the interface, and the source +generator + `[ConfigureHostBuilder]` attribute wire everything up automatically: + +```csharp +[BuildDefinition] +[GenerateEntryPoint] +internal partial class Build : WorkflowBuildDefinition, IGitVersion, IAzureKeyVault +{ + // All targets and parameters from IGitVersion and IAzureKeyVault + // are automatically available. +} +``` + +## Adding a Module + +1. Install the NuGet package: + + ```shell + dotnet add package Invex.Atom.Module.GitVersion + ``` + +2. Implement the module's interface on your build class: + + ```csharp + internal partial class Build : BuildDefinition, IGitVersion { } + ``` + +3. That's it. The module's services are registered, its targets are discoverable, and its parameters appear in help + output. + +## First-Party Modules + +| Package | Interface | Description | +|-------------------------------------|-------------------------|----------------------------------------------------| +| `Invex.Atom.Module.Dotnet` | *(various)* | .NET CLI helpers (build, test, pack, publish) | +| `Invex.Atom.Module.GithubWorkflows` | `IGithubWorkflows` | GitHub Actions workflow writer and helpers | +| `Invex.Atom.Module.DevopsWorkflows` | `IDevopsWorkflows` | Azure DevOps Pipelines workflow writer and helpers | +| `Invex.Atom.Module.AzureKeyVault` | `IAzureKeyVault` | Azure Key Vault secrets provider | +| `Invex.Atom.Module.AzureStorage` | `IAzureArtifactStorage` | Azure Blob Storage artifact provider | +| `Invex.Atom.Module.GitVersion` | `IGitVersion` | GitVersion-based build ID and version providers | + +## Module Conventions + +- Modules expose their functionality through **interfaces** that extend `IBuildAccessor`. +- Targets are defined as `Target` properties on the interface with default implementations. +- Parameters are declared with `[ParamDefinition]` or `[SecretDefinition]`. +- Service registration uses the `[ConfigureHostBuilder]` attribute with a static partial `ConfigureBuilder` method. +- Build options are extended via the static `BuildOptions` class using extension-like patterns. + +## Next Steps + +→ Individual module +pages: [.NET](dotnet.md) · [GitHub Workflows](github-workflows.md) · [DevOps Workflows](devops-workflows.md) · [Azure Key Vault](azure-key-vault.md) · [Azure Storage](azure-storage.md) · [GitVersion](git-version.md) + diff --git a/docs/reference/cli.md b/docs/reference/cli.md new file mode 100644 index 00000000..e1920ba2 --- /dev/null +++ b/docs/reference/cli.md @@ -0,0 +1,127 @@ +# CLI Reference + +Atom builds are invoked via `dotnet run` or the `atom` global tool. + +## Invocation + +### Via `dotnet run` + +```shell +dotnet run -- [options] [--param-name value ...] +``` + +### Via the `atom` Global Tool + +Install the tool: + +```shell +dotnet tool install --global Invex.Atom.Tool +``` + +Then run: + +```shell +atom [options] [--param-name value ...] +``` + +The tool discovers your Atom project (defaults to the `_atom` directory) and runs it. + +## Targets + +Specify one or more target names to execute: + +```shell +dotnet run -- Compile Test Pack +``` + +Targets execute in dependency order. Duplicates are resolved automatically. + +## Options + +| Option | Short | Description | +|--------------------|-------------|-------------------------------------------------------| +| `--help` | `-h` | Display help information | +| `--skip` | `-s` | Skip execution of dependent targets | +| `--headless` | `-hl` | Non-interactive mode (no prompts, plain output) | +| `--verbose` | `-v` | Enable verbose (debug-level) logging | +| `--interactive` | `-i` | Prompt for missing required parameters | +| `--project ` | `-p ` | Specify the Atom project directory (default: `_atom`) | + +## Parameters + +Pass parameter values with `-- `: + +```shell +dotnet run -- Deploy --api-key sk-123 --environment production +``` + +Parameter names use kebab-case on the command line and are matched to `[ParamDefinition]` attributes. + +## Examples + +```shell +# Run a single target +dotnet run -- Compile + +# Run multiple targets +dotnet run -- Compile Test + +# Pass parameters +dotnet run -- Deploy --configuration Release --api-key sk-123 + +# Interactive mode (prompt for missing params) +dotnet run -- Deploy -i + +# Skip dependencies +dotnet run -- Pack -s + +# Verbose output +dotnet run -- Compile -v + +# Use a custom project directory +dotnet run -- Compile -p MyBuildProject + +# Show help +dotnet run -- -h +``` + +## The `atom` Tool + +The `atom` global tool (`Invex.Atom.Tool`) provides the same interface but discovers your build project automatically: + +```shell +atom Compile Test --verbose +``` + +It searches for the Atom project in the current directory tree (or the directory specified by `-p`). + +### Restore & Build Caching + +Because a consuming project's Atom build is distributed as source, the `atom` tool would normally trigger a +`dotnet restore` and `dotnet build` on every invocation. To avoid this, the tool caches hashes of the relevant +inputs and skips work that isn't needed: + +- **Restore** is skipped (`--no-restore`) when the build project file plus any `Directory.Build.props`, + `Directory.Build.targets`, `Directory.Packages.props`, `nuget.config` and `global.json` files (found while + walking up to the project root) are unchanged. +- **Build** is skipped (`--no-build`) when, in addition to the restore inputs, every `.cs` source file under the + build project (excluding `bin`/`obj`) is unchanged and a previous build output exists. `--no-build` implies + `--no-restore`. + +The hashes are stored in the build project's `obj/.atom-restore.hash` and `obj/.atom-build.hash` files, so they +are automatically invalidated by `dotnet clean` and never committed to source control. + +> Note: source changes in projects referenced via `` are not tracked by the build cache. If your +> build project references other projects by source, force a full build with the opt-out below. + +To force a full restore and build regardless of the caches, use either: + +```shell +# CLI flag +atom Compile --no-restore-cache + +# Environment variable (any value other than "0"/"false" enables the opt-out) +ATOM_NO_RESTORE_CACHE=1 atom Compile +``` + + diff --git a/docs/toc.yml b/docs/toc.yml new file mode 100644 index 00000000..2a81276d --- /dev/null +++ b/docs/toc.yml @@ -0,0 +1,98 @@ +- name: Getting Started + href: getting-started/introduction.md + items: + - name: Introduction + href: getting-started/introduction.md + - name: Your First Build + href: getting-started/your-first-build.md + - name: Base vs Workflow Build + href: getting-started/base-vs-workflow-build.md +- name: Core Concepts + href: core-concepts/build-definitions.md + items: + - name: Build Definitions + href: core-concepts/build-definitions.md + - name: Targets + href: core-concepts/targets.md + - name: Parameters + href: core-concepts/parameters.md + - name: Secrets + href: core-concepts/secrets.md + - name: Artifacts + href: core-concepts/artifacts.md + - name: Variables + href: core-concepts/variables.md + - name: File System + href: core-concepts/file-system.md + - name: Process Runner + href: core-concepts/process-runner.md + - name: Build Info + href: core-concepts/build-info.md + - name: Build Options + href: core-concepts/build-options.md + - name: Hosting + href: core-concepts/hosting.md + - name: Lifecycle Hooks + href: core-concepts/lifecycle-hooks.md + - name: Logging & Reports + href: core-concepts/logging-and-reports.md + - name: File Transformations + href: core-concepts/file-transformations.md +- name: Workflows + href: workflows/overview.md + items: + - name: Overview + href: workflows/overview.md + - name: Workflow Definitions + href: workflows/workflow-definitions.md + - name: Triggers + href: workflows/triggers.md + - name: Workflow Options + href: workflows/workflow-options.md + - name: Variables in Workflows + href: workflows/variables-in-workflows.md + - name: Debugging Workflows + href: workflows/debugging-workflows.md +- name: Modules + href: modules/overview.md + items: + - name: Overview + href: modules/overview.md + - name: .NET + href: modules/dotnet.md + - name: GitHub Workflows + href: modules/github-workflows.md + - name: DevOps Workflows + href: modules/devops-workflows.md + - name: Azure Key Vault + href: modules/azure-key-vault.md + - name: Azure Storage + href: modules/azure-storage.md + - name: GitVersion + href: modules/git-version.md +- name: Built-in Targets + href: built-in-targets/setup-build-info.md + items: + - name: SetupBuildInfo + href: built-in-targets/setup-build-info.md + - name: ValidateBuild + href: built-in-targets/validate-build.md + - name: GenerateWorkflowFiles + href: built-in-targets/generate-workflow-files.md +- name: Developer Guide + href: developer-guide/writing-a-module.md + items: + - name: Writing a Module + href: developer-guide/writing-a-module.md + - name: Custom Providers + href: developer-guide/custom-providers.md + - name: Source Generators + href: developer-guide/source-generators.md + - name: Testing + href: developer-guide/testing.md +- name: Reference + href: reference/cli.md + items: + - name: CLI + href: reference/cli.md + diff --git a/docs/workflows/debugging-workflows.md b/docs/workflows/debugging-workflows.md new file mode 100644 index 00000000..e6003320 --- /dev/null +++ b/docs/workflows/debugging-workflows.md @@ -0,0 +1,35 @@ +# Debugging Workflows + +Atom includes a `DebugWorkflowType` that lets you simulate workflow generation locally without pushing to CI. + +## `DebugWorkflowType` + +`DebugWorkflowType` is an `IWorkflowType` where `IsRunning` is always `true`. When added to your workflow's `Types`, it +triggers the `DebugWorkflowWriter` which writes the resolved workflow model as JSON to a `.debug-workflows` directory — +useful for inspecting exactly what Atom generates. + +## Running Locally + +When you execute `dotnet run -- GenerateWorkflowFiles` (or `Gen`), the platform-specific writers produce the actual YAML +files. You can compare these against your expectations by: + +1. Running `Gen` and diffing the generated YAML. +2. Adding `DebugWorkflowType` temporarily to inspect the internal model. + +## Checking for Outdated Workflows + +The `WorkflowLifecycleHook` runs during `BeforeExecute` and detects when the YAML on disk doesn't match what the current +build definition would generate. This helps catch cases where you've changed targets or workflows but forgotten to +regenerate. + +## Tips + +- Use `--verbose` to see detailed resolution logs. +- Inspect the generated YAML directly — it's meant to be human-readable. +- The workflow resolver will log warnings for missing variable/artifact declarations. +- All your targets execute the same code locally and on CI; only the variable/artifact transport differs. + +## Next Steps + +→ [Modules Overview](../modules/overview.md) + diff --git a/docs/workflows/overview.md b/docs/workflows/overview.md new file mode 100644 index 00000000..9984621e --- /dev/null +++ b/docs/workflows/overview.md @@ -0,0 +1,54 @@ +# Workflows Overview + +Workflows are an optional layer on top of the base build system. They let you define how your targets map to CI/CD jobs +and automatically generate the platform-specific YAML files for GitHub Actions and Azure DevOps Pipelines. + +## When Do You Need Workflows? + +Use workflows when you want Atom to **generate** your CI/CD configuration. If you only run builds locally or maintain +your YAML by hand, stick with `BuildDefinition`. + +## Enabling Workflows + +1. Inherit from `WorkflowBuildDefinition` instead of `BuildDefinition`: + + ```csharp + [BuildDefinition] + [GenerateEntryPoint] + internal partial class Build : WorkflowBuildDefinition + { + // ... + } + ``` + +2. Override the `Workflows` property to declare your pipelines. + +3. Add a platform module (`Invex.Atom.Module.GithubWorkflows` or `Invex.Atom.Module.DevopsWorkflows`) so Atom knows + which YAML format to emit. + +4. Run `dotnet run -- GenerateWorkflowFiles` (alias: `Gen`) to write the files. + +## What `WorkflowBuildDefinition` Adds + +`WorkflowBuildDefinition` extends `BuildDefinition` with: + +| Feature | Description | +|--------------------------|--------------------------------------------------------------------------------| +| `Workflows` property | An `IReadOnlyList` describing each CI/CD pipeline. | +| `IGenerateWorkflowFiles` | The `GenerateWorkflowFiles` target that writes YAML. | +| `WorkflowGenerator` | Resolves the build model into a platform-neutral workflow model. | +| `WorkflowResolver` | Analyses target dependencies, artifacts, and variables to build the job graph. | +| `WorkflowLifecycleHook` | A lifecycle hook that warns when generated files are outdated. | + +## How Generation Works + +1. The `WorkflowResolver` examines your `Workflows` definitions and your target dependency graph. +2. It groups targets into **jobs**, splitting on artifact/variable boundaries (since those require separate CI jobs). +3. The `WorkflowGenerator` maps each job to a `WorkflowModel`. +4. A platform-specific `WorkflowFileWriter` (e.g. for GitHub Actions or Azure DevOps) renders the model to YAML and + writes it to disk. + +## Next Steps + +→ [Workflow Definitions](workflow-definitions.md) + diff --git a/docs/workflows/triggers.md b/docs/workflows/triggers.md new file mode 100644 index 00000000..e0580986 --- /dev/null +++ b/docs/workflows/triggers.md @@ -0,0 +1,101 @@ +# Triggers + +Triggers define when a workflow starts. Atom provides a fluent API via the `WorkflowTriggers` helper class. + +## Built-in Trigger Shortcuts + +| Shortcut | Trigger Type | Description | +|---------------------------------|-------------------------|-------------------------------------| +| `WorkflowTriggers.Manual` | `ManualTrigger` | Manual dispatch (workflow_dispatch) | +| `WorkflowTriggers.PushToMain` | `GitPushTrigger` | Push to `main` branch | +| `WorkflowTriggers.PullIntoMain` | `GitPullRequestTrigger` | Pull request targeting `main` | + +## Git Push Trigger + +```csharp +// Push to specific branches +WorkflowTriggers.PushTo("main", "release/**") + +// Full control +WorkflowTriggers.Push( + includedBranches: ["main"], + excludedBranches: [], + includedPaths: ["src/**"], + excludedPaths: ["docs/**"], + includedTags: ["v*"], + excludedTags: []) +``` + +### `GitPushTrigger` Properties + +| Property | Description | +|--------------------|----------------------------------------| +| `IncludedBranches` | Branches that trigger the workflow | +| `ExcludedBranches` | Branches to exclude | +| `IncludedPaths` | File paths that trigger the workflow | +| `ExcludedPaths` | File paths to exclude | +| `IncludedTags` | Tag patterns that trigger the workflow | +| `ExcludedTags` | Tag patterns to exclude | + +## Git Pull Request Trigger + +```csharp +// PR targeting specific branches +WorkflowTriggers.PullInto("main", "release/**") + +// Full control +WorkflowTriggers.PullRequest( + includedBranches: ["main"], + excludedBranches: [], + includedPaths: ["src/**"], + excludedPaths: [], + types: ["opened", "synchronize"]) +``` + +## Manual Trigger + +```csharp +// Simple manual dispatch +WorkflowTriggers.Manual + +// With inputs +WorkflowTriggers.ManualWithInputs( + new ManualStringInput("version", "Version to deploy"), + new ManualBoolInput("dry-run", "Dry run mode"), + new ManualChoiceInput("environment", "Target environment", ["staging", "production"])) +``` + +### Manual Input Types + +| Type | Description | +|---------------------|---------------------------------| +| `ManualStringInput` | Free-text string input | +| `ManualBoolInput` | Boolean toggle | +| `ManualChoiceInput` | Dropdown with predefined values | + +## Platform-Specific Triggers + +Platform modules can provide additional trigger types. For example, `Invex.Atom.Module.GithubWorkflows` adds +`GithubTrigger` for events like `release`: + +```csharp +new GithubTrigger(new On.Release([On.Release.ReleaseType.released])) +``` + +## Combining Triggers + +A workflow can have multiple triggers: + +```csharp +Triggers = +[ + WorkflowTriggers.Manual, + WorkflowTriggers.PushToMain, + WorkflowTriggers.PullIntoMain, +], +``` + +## Next Steps + +→ [Workflow Options](workflow-options.md) + diff --git a/docs/workflows/variables-in-workflows.md b/docs/workflows/variables-in-workflows.md new file mode 100644 index 00000000..2b5686ad --- /dev/null +++ b/docs/workflows/variables-in-workflows.md @@ -0,0 +1,53 @@ +# Variables in Workflows + +When running locally, variables are shared in-memory between targets. In CI/CD workflows, targets may run in separate +jobs on different machines. Atom bridges this gap by mapping variables to the platform's native output/variable +mechanism. + +## How It Works + +1. A target declares `.ProducesVariable("BuildVersion")` and writes it with `WriteVariable`. +2. A downstream target declares `.ConsumesVariable(nameof(SetupBuildInfo), "BuildVersion")`. +3. **Locally**: the variable is stored in-memory and read directly. +4. **In CI**: the `WorkflowResolver` detects the dependency, creates a job boundary if needed, and the platform module + maps the variable to: + - **GitHub Actions**: job outputs (`${{ needs.job.outputs.var }}`) + - **Azure DevOps**: pipeline variables + +## Platform Variable Providers + +Each platform module registers an `IVariableProvider`: + +| Module | Provider | Mechanism | +|-------------------------------------|--------------------------|----------------------------------------------------| +| `Invex.Atom.Module.GithubWorkflows` | `GithubVariableProvider` | Writes to `$GITHUB_OUTPUT`, reads from job outputs | +| `Invex.Atom.Module.DevopsWorkflows` | `DevopsVariableProvider` | Uses Azure DevOps pipeline variables | + +The built-in `AtomVariableProvider` handles local execution. + +## Example + +```csharp +Target SetupVersion => t => t + .ProducesVariable("BuildVersion") + .Executes(async ct => + { + await WriteVariable("BuildVersion", "1.2.3", ct); + }); + +Target Pack => t => t + .ConsumesVariable(nameof(SetupVersion), "BuildVersion") + .DependsOn(SetupVersion) + .Executes(async ct => + { + var version = await ReadVariable(nameof(SetupVersion), "BuildVersion", ct); + // use version + }); +``` + +In the generated YAML, `SetupVersion` and `Pack` will be in separate jobs with the variable passed as a job output. + +## Next Steps + +→ [Debugging Workflows](debugging-workflows.md) + diff --git a/docs/workflows/workflow-definitions.md b/docs/workflows/workflow-definitions.md new file mode 100644 index 00000000..b08cd807 --- /dev/null +++ b/docs/workflows/workflow-definitions.md @@ -0,0 +1,141 @@ +# Workflow Definitions + +A `WorkflowDefinition` describes a single CI/CD pipeline — its name, triggers, targets, options, and which platforms it +applies to. + +## Basic Structure + +```csharp +public override IReadOnlyList Workflows => +[ + new("CI") + { + Triggers = [WorkflowTriggers.PushToMain, WorkflowTriggers.PullIntoMain], + Targets = + [ + new(nameof(SetupBuildInfo)), + new(nameof(Compile)), + new(nameof(Test)), + ], + Types = [WorkflowTypes.Github.Action], + }, +]; +``` + +## `WorkflowDefinition` Properties + +| Property | Type | Description | +|------------|-------------------------------------------|------------------------------------------------| +| `Name` | `string` | The workflow name (used as the file name). | +| `Triggers` | `IReadOnlyList` | Events that start the workflow. | +| `Targets` | `IReadOnlyList` | The targets to include in the workflow. | +| `Options` | `IReadOnlyList` | Workflow-level options applied to all targets. | +| `Types` | `IReadOnlyList` | Which platforms to generate for. | + +## `WorkflowTargetDefinition` + +Each entry in `Targets` is a `WorkflowTargetDefinition`: + +```csharp +new(nameof(Test)) +{ + MatrixDimensions = + [ + new(nameof(IJobRunsOn.JobRunsOn)) + { + Values = ["ubuntu-latest", "windows-latest"], + }, + ], + Options = [BuildOptions.Github.RunsOn.SetByMatrix], +} +``` + +| Property | Description | +|--------------------|---------------------------------------------------------------------| +| `Name` | The target name (must match a target in the build definition). | +| `MatrixDimensions` | Run the target across multiple configurations (OS, framework, etc). | +| `Options` | Per-target options (suppress artifacts, inject params, etc). | + +### Matrix Dimensions + +A `MatrixDimension` defines a named axis with multiple values. The CI platform runs the target once per combination: + +```csharp +new MatrixDimension(nameof(IJobRunsOn.JobRunsOn)) +{ + Values = ["ubuntu-latest", "windows-latest", "macos-latest"], +} +``` + +Multiple dimensions create a full cross product. + +## Workflow Types + +The `Types` list determines which platforms receive generated files: + +| Type | Platform | +|---------------------------------|---------------------------------------| +| `WorkflowTypes.Github.Action` | GitHub Actions (`.github/workflows/`) | +| `WorkflowTypes.Devops.Pipeline` | Azure DevOps Pipelines | + +You can target multiple platforms from the same workflow definition. + +## Workflow-Level Options + +Options applied at the workflow level affect all targets in the workflow: + +```csharp +new("Build") +{ + Options = + [ + BuildOptions.Inject.Param(nameof(NugetDryRun), true), + BuildOptions.Devops.VariableGroup.Atom, + ], + // ... +} +``` + +## Real-World Example + +From the Atom project's own build: + +```csharp +new("Validate") +{ + Triggers = [WorkflowTriggers.Manual, WorkflowTriggers.PullIntoMain], + Targets = + [ + new(nameof(ISetupBuildInfo.SetupBuildInfo)), + new(nameof(IBuildTargets.PackProjects)) + { + Options = [BuildOptions.Target.SuppressArtifactPublishing], + }, + new(nameof(ITestTargets.TestProjects)) + { + MatrixDimensions = + [ + new(nameof(IJobRunsOn.JobRunsOn)) + { + Values = ["windows-latest", "ubuntu-latest", "macos-latest"], + }, + new(nameof(ITestTargets.TestFramework)) + { + Values = ["net8.0", "net9.0", "net10.0"], + }, + ], + Options = + [ + BuildOptions.Target.SuppressArtifactPublishing, + BuildOptions.Github.RunsOn.SetByMatrix, + ], + }, + ], + Types = [WorkflowTypes.Github.Action], +} +``` + +## Next Steps + +→ [Triggers](triggers.md) + diff --git a/docs/workflows/workflow-options.md b/docs/workflows/workflow-options.md new file mode 100644 index 00000000..5447c321 --- /dev/null +++ b/docs/workflows/workflow-options.md @@ -0,0 +1,123 @@ +# Workflow Options + +Workflow options fine-tune how targets behave when running as CI/CD jobs. They can be applied at the workflow level ( +affecting all targets) or per-target. + +## Common Options + +### Suppress Artifact Publishing + +Prevents a target from uploading artifacts in CI (useful for validation workflows): + +```csharp +BuildOptions.Target.SuppressArtifactPublishing +``` + +### Inject Parameters and Secrets + +Inject a parameter value at workflow level: + +```csharp +BuildOptions.Inject.Param(nameof(NugetDryRun), true) +``` + +Inject a secret from the CI platform's secret store: + +```csharp +BuildOptions.Inject.Secret(nameof(IGithubHelper.GithubToken)) +``` + +### Runner Selection + +Set the runner from a matrix dimension: + +```csharp +BuildOptions.Github.RunsOn.SetByMatrix +BuildOptions.Devops.DevopsPool.SetByMatrix +``` + +### Setup Steps + +Add pre-job steps like installing a specific .NET SDK: + +```csharp +BuildOptions.Steps.SetupDotnet.Dotnet80X() +BuildOptions.Steps.SetupDotnet.Dotnet90X() +BuildOptions.Steps.SetupDotnet.Dotnet100X() +``` + +### Checkout Configuration + +Configure the checkout step for the workflow. + +### Deploy to Environment + +Target a specific deployment environment (with approvals, gates, etc.): + +```csharp +new DeployToEnvironment("production") +``` + +### Conditional Execution + +Run a target only when a condition is met: + +```csharp +BuildOptions.Target.RunIfWorkflowCondition( + TextExpressions.Github.GithubActor.EqualToString("dependabot[bot]")) +``` + +### GitHub Token Permissions + +Set fine-grained permissions for the `GITHUB_TOKEN`: + +```csharp +new GithubTokenPermissionsOption(new Permissions.Exact(new() +{ + Contents = PermissionsLevel.Write, + PullRequests = PermissionsLevel.Write, +})) +``` + +## Applying Options + +### Workflow-Level + +```csharp +new("Build") +{ + Options = + [ + BuildOptions.Inject.Param(nameof(NugetDryRun), true), + ], + // ... +} +``` + +### Per-Target + +```csharp +new(nameof(PackTool)) +{ + Options = + [ + BuildOptions.Target.SuppressArtifactPublishing, + BuildOptions.Github.RunsOn.SetByMatrix, + ], +} +``` + +## Custom Options + +Implement `IBuildOption` to create your own: + +```csharp +public sealed record MyCustomOption(string Value) : IBuildOption; +``` + +Platform modules can then inspect the options during YAML generation. + +## Next Steps + +→ [Variables in Workflows](variables-in-workflows.md) + diff --git a/index.md b/index.md new file mode 100644 index 00000000..f27da7b0 --- /dev/null +++ b/index.md @@ -0,0 +1,8 @@ +--- +uid: index +--- + + + +Redirecting to [documentation home](README.md)... + diff --git a/DecSm.Atom.Analyzers.Sample/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParam.cs b/samples/Invex.Atom.Analyzers.Sample/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParam.cs similarity index 92% rename from DecSm.Atom.Analyzers.Sample/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParam.cs rename to samples/Invex.Atom.Analyzers.Sample/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParam.cs index 2bbdc998..3cdd0fb1 100644 --- a/DecSm.Atom.Analyzers.Sample/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParam.cs +++ b/samples/Invex.Atom.Analyzers.Sample/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParam.cs @@ -1,12 +1,14 @@ -using DecSm.Atom.Build; -using DecSm.Atom.Build.Definition; -using DecSm.Atom.Params; +#pragma warning disable AT0001 + +using Invex.Atom.Build; +using Invex.Atom.Build.Definition; +using Invex.Atom.Build.Params; using JetBrains.Annotations; -namespace DecSm.Atom.Analyzers.Sample; +namespace Invex.Atom.Analyzers.Sample; /// -/// This interface serves as a sample for the DecSm.Atom analyzer AT0001. +/// This interface serves as a sample for the Invex.Atom analyzer AT0001. /// /// /// Analyzer AT0001 flags targets that directly reference a parameter property within the @@ -81,3 +83,5 @@ public interface IMyTarget : IBuildAccessor .RequiresParam(MyParam1) // Analyzer AT0001 should flag this .RequiresParam(NotParam2); } + +#pragma warning restore AT0001 diff --git a/samples/Invex.Atom.Analyzers.Sample/Invex.Atom.Analyzers.Sample.csproj b/samples/Invex.Atom.Analyzers.Sample/Invex.Atom.Analyzers.Sample.csproj new file mode 100644 index 00000000..668d33e5 --- /dev/null +++ b/samples/Invex.Atom.Analyzers.Sample/Invex.Atom.Analyzers.Sample.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + false + + + + + + + + + diff --git a/Sample_01_HelloWorld/Build.cs b/samples/Sample_01_HelloWorld/Build.cs similarity index 87% rename from Sample_01_HelloWorld/Build.cs rename to samples/Sample_01_HelloWorld/Build.cs index 5149590a..12e04bb0 100644 --- a/Sample_01_HelloWorld/Build.cs +++ b/samples/Sample_01_HelloWorld/Build.cs @@ -2,15 +2,15 @@ // In the console, run the following command to execute the build: // dotnet run -- HelloWorld -// This is automatically globally included when using DecSm.Atom from a nuget package +// These usings are automatically globally included when using Invex.Atom from a nuget package -using DecSm.Atom.Build.Definition; -using DecSm.Atom.Hosting; +using Invex.Atom.Build.Definition; +using Invex.Atom.Build.Hosting; namespace Atom; /// -/// This build definition provides a minimal example of how to create a build process using DecSm.Atom. +/// This build definition provides a minimal example of how to create a build process using Invex.Atom. /// It defines a single target, , which prints a "Hello, World!" message to the console. /// /// diff --git a/samples/Sample_01_HelloWorld/Sample_01_HelloWorld.csproj b/samples/Sample_01_HelloWorld/Sample_01_HelloWorld.csproj new file mode 100644 index 00000000..220f1779 --- /dev/null +++ b/samples/Sample_01_HelloWorld/Sample_01_HelloWorld.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + Atom + + + + + + + + diff --git a/Sample_02_Params/Build.cs b/samples/Sample_02_Params/Build.cs similarity index 92% rename from Sample_02_Params/Build.cs rename to samples/Sample_02_Params/Build.cs index 870656f3..a0a78126 100644 --- a/Sample_02_Params/Build.cs +++ b/samples/Sample_02_Params/Build.cs @@ -14,16 +14,16 @@ // or // dotnet run -- Hello -i -// These are automatically globally included when using DecSm.Atom from a nuget package +// These usings are automatically globally included when using Invex.Atom from a nuget package -using DecSm.Atom.Build.Definition; -using DecSm.Atom.Hosting; -using DecSm.Atom.Params; +using Invex.Atom.Build.Definition; +using Invex.Atom.Build.Hosting; +using Invex.Atom.Build.Params; namespace Atom; /// -/// This build definition demonstrates how to define and use parameters within a DecSm.Atom build process. +/// This build definition demonstrates how to define and use parameters within a Invex.Atom build process. /// It showcases required parameters, parameters with default values, and how to retrieve configuration from /// `appsettings.json`. /// @@ -39,7 +39,7 @@ namespace Atom; /// dotnet run -- Hello --my-name World /// /// -/// To have DecSm.Atom interactively prompt for required parameters, use the `--interactive` or `-i` flag: +/// To have Invex.Atom interactively prompt for required parameters, use the `--interactive` or `-i` flag: /// dotnet run -- Hello --interactive /// or /// dotnet run -- Hello -i @@ -63,7 +63,7 @@ internal partial class Build : BuildDefinition /// Defines a parameter named "config-item-1" which is populated from `appsettings.json`. /// /// - /// This property demonstrates how DecSm.Atom can automatically bind configuration values + /// This property demonstrates how Invex.Atom can automatically bind configuration values /// from `appsettings.json` to build parameters. The name "config-item-1" in the `ParamDefinition` /// attribute corresponds to a key in the `appsettings.json` file. /// diff --git a/samples/Sample_02_Params/Sample_02_Params.csproj b/samples/Sample_02_Params/Sample_02_Params.csproj new file mode 100644 index 00000000..220f1779 --- /dev/null +++ b/samples/Sample_02_Params/Sample_02_Params.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + Atom + + + + + + + + diff --git a/Sample_02_Params/appsettings.json b/samples/Sample_02_Params/appsettings.json similarity index 100% rename from Sample_02_Params/appsettings.json rename to samples/Sample_02_Params/appsettings.json diff --git a/DecSm.Atom.Analyzers/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzer.cs b/src/Invex.Atom.Build.Analyzers/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzer.cs similarity index 99% rename from DecSm.Atom.Analyzers/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzer.cs rename to src/Invex.Atom.Build.Analyzers/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzer.cs index a79bf1d8..3ae2cde3 100644 --- a/DecSm.Atom.Analyzers/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzer.cs +++ b/src/Invex.Atom.Build.Analyzers/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzer.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Analyzers; +namespace Invex.Atom.Build.Analyzers; // ReSharper disable once InconsistentNaming /// diff --git a/DecSm.Atom.Analyzers/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProvider.cs b/src/Invex.Atom.Build.Analyzers/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProvider.cs similarity index 98% rename from DecSm.Atom.Analyzers/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProvider.cs rename to src/Invex.Atom.Build.Analyzers/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProvider.cs index 696fc3f9..17f50022 100644 --- a/DecSm.Atom.Analyzers/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProvider.cs +++ b/src/Invex.Atom.Build.Analyzers/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Analyzers; +namespace Invex.Atom.Build.Analyzers; // ReSharper disable once InconsistentNaming /// diff --git a/src/Invex.Atom.Build.Analyzers/AT0002_ConfigureHostPartialMethodNotImplementedAnalyzer.cs b/src/Invex.Atom.Build.Analyzers/AT0002_ConfigureHostPartialMethodNotImplementedAnalyzer.cs new file mode 100644 index 00000000..cfd57ca3 --- /dev/null +++ b/src/Invex.Atom.Build.Analyzers/AT0002_ConfigureHostPartialMethodNotImplementedAnalyzer.cs @@ -0,0 +1,121 @@ +namespace Invex.Atom.Build.Analyzers; + +// ReSharper disable once InconsistentNaming +/// +/// Analyzer that reports when an interface decorated with [ConfigureHost] is missing +/// the required partial method implementation ConfigureHostFrom{InterfaceName}. +/// +#pragma warning disable RS1038 +[DiagnosticAnalyzer(LanguageNames.CSharp)] +#pragma warning restore RS1038 +public class AT0002_ConfigureHostPartialMethodNotImplementedAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "AT0002"; + + private const string Category = "Usage"; + + private const string ConfigureHostAttributeFull = "Invex.Atom.Build.Hosting.ConfigureHostAttribute"; + + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AT0002Title), + Resources.ResourceManager, + typeof(Resources)); + + private static readonly LocalizableString MessageFormat = + new LocalizableResourceString(nameof(Resources.AT0002MessageFormat), + Resources.ResourceManager, + typeof(Resources)); + + private static readonly LocalizableString Description = + new LocalizableResourceString(nameof(Resources.AT0002Description), + Resources.ResourceManager, + typeof(Resources)); + + private static readonly DiagnosticDescriptor Rule = new(DiagnosticId, + Title, + MessageFormat, + Category, + DiagnosticSeverity.Warning, + true, + Description); + + public override ImmutableArray SupportedDiagnostics { get; } = [Rule]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType); + } + + private static void AnalyzeSymbol(SymbolAnalysisContext context) + { + if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Interface } interfaceSymbol) + return; + + var hasAttribute = false; + + foreach (var attr in interfaceSymbol.GetAttributes()) + if (attr.AttributeClass?.ToDisplayString() == ConfigureHostAttributeFull) + { + hasAttribute = true; + + break; + } + + if (!hasAttribute) + return; + + var expectedMethodName = $"ConfigureHostFrom{interfaceSymbol.Name}"; + + if (HasMethodWithBody(interfaceSymbol, expectedMethodName, context.CancellationToken)) + return; + + var properties = new Dictionary + { + { "methodName", expectedMethodName }, + }.ToImmutableDictionary(); + + var diagnostic = Diagnostic.Create(Rule, + interfaceSymbol.Locations[0], + properties, + interfaceSymbol.Name, + expectedMethodName); + + context.ReportDiagnostic(diagnostic); + } + + internal static bool HasMethodWithBody( + INamedTypeSymbol interfaceSymbol, + string expectedMethodName, + CancellationToken cancellationToken) + { + foreach (var member in interfaceSymbol.GetMembers()) + { + if (member is not IMethodSymbol methodSymbol || methodSymbol.Name != expectedMethodName) + continue; + + // Check the method's own declaring syntax references for a body + foreach (var syntaxRef in methodSymbol.DeclaringSyntaxReferences) + { + var syntax = syntaxRef.GetSyntax(cancellationToken); + + if (syntax is MethodDeclarationSyntax methodSyntax && + (methodSyntax.Body != null || methodSyntax.ExpressionBody != null)) + return true; + } + + // For partial methods, also check the implementation part + if (methodSymbol.PartialImplementationPart != null) + foreach (var syntaxRef in methodSymbol.PartialImplementationPart.DeclaringSyntaxReferences) + { + var syntax = syntaxRef.GetSyntax(cancellationToken); + + if (syntax is MethodDeclarationSyntax methodSyntax && + (methodSyntax.Body != null || methodSyntax.ExpressionBody != null)) + return true; + } + } + + return false; + } +} diff --git a/src/Invex.Atom.Build.Analyzers/AT0002_ConfigureHostPartialMethodNotImplementedCodeFixProvider.cs b/src/Invex.Atom.Build.Analyzers/AT0002_ConfigureHostPartialMethodNotImplementedCodeFixProvider.cs new file mode 100644 index 00000000..57432894 --- /dev/null +++ b/src/Invex.Atom.Build.Analyzers/AT0002_ConfigureHostPartialMethodNotImplementedCodeFixProvider.cs @@ -0,0 +1,124 @@ +using Microsoft.CodeAnalysis.Formatting; + +namespace Invex.Atom.Build.Analyzers; + +// ReSharper disable once InconsistentNaming +/// +/// Provides a code fix for interfaces decorated with [ConfigureHost] that are missing +/// the required partial method implementation, by inserting the method stub. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, + Name = nameof(AT0002_ConfigureHostPartialMethodNotImplementedCodeFixProvider))] +[Shared] +public class AT0002_ConfigureHostPartialMethodNotImplementedCodeFixProvider : CodeFixProvider +{ + private const string HostUsing = "Microsoft.Extensions.Hosting"; + + public sealed override ImmutableArray FixableDiagnosticIds { get; } = + [ + AT0002_ConfigureHostPartialMethodNotImplementedAnalyzer.DiagnosticId, + ]; + + public sealed override FixAllProvider GetFixAllProvider() => + WellKnownFixAllProviders.BatchFixer; + + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.First(); + var methodName = diagnostic.Properties["methodName"] ?? "ConfigureHost"; + + context.RegisterCodeFix(CodeAction.Create(string.Format(Resources.AT0002CodeFixTitle, methodName), + c => AddMethodImplementationAsync(context.Document, diagnostic, c), + nameof(Resources.AT0002CodeFixTitle)), + diagnostic); + + return Task.CompletedTask; + } + + private static async Task AddMethodImplementationAsync( + Document document, + Diagnostic diagnostic, + CancellationToken cancellationToken) + { + var root = await document + .GetSyntaxRootAsync(cancellationToken) + .ConfigureAwait(false); + + if (root == null) + return document; + + var methodName = diagnostic.Properties["methodName"] ?? "ConfigureHost"; + + var diagnosticSpan = diagnostic.Location.SourceSpan; + var node = root.FindNode(diagnosticSpan); + + // Find the interface declaration + var interfaceDeclaration = node.FirstAncestorOrSelf(); + + if (interfaceDeclaration == null) + return document; + + // Create the method declaration: + // protected static partial void ConfigureHostFrom{InterfaceName}(IHost host) + // { + // } + var method = SyntaxFactory + .MethodDeclaration(SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), + SyntaxFactory.Identifier(methodName)) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.ProtectedKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword), + SyntaxFactory.Token(SyntaxKind.PartialKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(new[] + { + SyntaxFactory + .Parameter(SyntaxFactory.Identifier("host")) + .WithType(SyntaxFactory.IdentifierName("IHost")), + }))) + .WithBody(SyntaxFactory.Block()) + .NormalizeWhitespace(); + + // Convert semicolon-terminated empty type to brace-delimited type + var originalInterfaceDeclaration = interfaceDeclaration; + + if (interfaceDeclaration.SemicolonToken != default) + interfaceDeclaration = interfaceDeclaration + .WithSemicolonToken(default) + .WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken)) + .WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken)); + + // Add the method to the interface + var newInterfaceDeclaration = interfaceDeclaration.AddMembers(method); + var newRoot = root.ReplaceNode(originalInterfaceDeclaration, newInterfaceDeclaration); + + // Add using directive if not present + if (newRoot is CompilationUnitSyntax compilationUnit && + compilationUnit.Usings.All(u => u.Name?.ToString() != HostUsing)) + { + var eol = root + .DescendantTrivia() + .Where(t => t.IsKind(SyntaxKind.EndOfLineTrivia)) + .Select(t => t.ToString()) + .FirstOrDefault() ?? + "\n"; + + var usingDirective = SyntaxFactory + .UsingDirective(SyntaxFactory.ParseName(HostUsing)) + .NormalizeWhitespace() + .WithTrailingTrivia(SyntaxFactory.EndOfLine(eol)); + + newRoot = compilationUnit.AddUsings(usingDirective); + } + + var result = document.WithSyntaxRoot(newRoot); + + var resultRoot = await result + .GetSyntaxRootAsync(cancellationToken) + .ConfigureAwait(false) ?? + throw new InvalidOperationException(); + + var formattedRoot = Formatter.Format(resultRoot.WithAdditionalAnnotations(Formatter.Annotation), + document.Project.Solution.Workspace); + + return result.WithSyntaxRoot(formattedRoot); + } +} diff --git a/src/Invex.Atom.Build.Analyzers/AT0003_ConfigureHostBuilderPartialMethodNotImplementedAnalyzer.cs b/src/Invex.Atom.Build.Analyzers/AT0003_ConfigureHostBuilderPartialMethodNotImplementedAnalyzer.cs new file mode 100644 index 00000000..97b0f53c --- /dev/null +++ b/src/Invex.Atom.Build.Analyzers/AT0003_ConfigureHostBuilderPartialMethodNotImplementedAnalyzer.cs @@ -0,0 +1,88 @@ +namespace Invex.Atom.Build.Analyzers; + +// ReSharper disable once InconsistentNaming +/// +/// Analyzer that reports when an interface decorated with [ConfigureHostBuilder] is missing +/// the required partial method implementation ConfigureBuilderFrom{InterfaceName}. +/// +#pragma warning disable RS1038 +[DiagnosticAnalyzer(LanguageNames.CSharp)] +#pragma warning restore RS1038 +public class AT0003_ConfigureHostBuilderPartialMethodNotImplementedAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "AT0003"; + + private const string Category = "Usage"; + + private const string ConfigureHostBuilderAttributeFull = "Invex.Atom.Build.Hosting.ConfigureHostBuilderAttribute"; + + private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AT0003Title), + Resources.ResourceManager, + typeof(Resources)); + + private static readonly LocalizableString MessageFormat = + new LocalizableResourceString(nameof(Resources.AT0003MessageFormat), + Resources.ResourceManager, + typeof(Resources)); + + private static readonly LocalizableString Description = + new LocalizableResourceString(nameof(Resources.AT0003Description), + Resources.ResourceManager, + typeof(Resources)); + + private static readonly DiagnosticDescriptor Rule = new(DiagnosticId, + Title, + MessageFormat, + Category, + DiagnosticSeverity.Warning, + true, + Description); + + public override ImmutableArray SupportedDiagnostics { get; } = [Rule]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType); + } + + private static void AnalyzeSymbol(SymbolAnalysisContext context) + { + if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Interface } interfaceSymbol) + return; + + var hasAttribute = false; + + foreach (var attr in interfaceSymbol.GetAttributes()) + if (attr.AttributeClass?.ToDisplayString() == ConfigureHostBuilderAttributeFull) + { + hasAttribute = true; + + break; + } + + if (!hasAttribute) + return; + + var expectedMethodName = $"ConfigureBuilderFrom{interfaceSymbol.Name}"; + + if (AT0002_ConfigureHostPartialMethodNotImplementedAnalyzer.HasMethodWithBody(interfaceSymbol, + expectedMethodName, + context.CancellationToken)) + return; + + var properties = new Dictionary + { + { "methodName", expectedMethodName }, + }.ToImmutableDictionary(); + + var diagnostic = Diagnostic.Create(Rule, + interfaceSymbol.Locations[0], + properties, + interfaceSymbol.Name, + expectedMethodName); + + context.ReportDiagnostic(diagnostic); + } +} diff --git a/src/Invex.Atom.Build.Analyzers/AT0003_ConfigureHostBuilderPartialMethodNotImplementedCodeFixProvider.cs b/src/Invex.Atom.Build.Analyzers/AT0003_ConfigureHostBuilderPartialMethodNotImplementedCodeFixProvider.cs new file mode 100644 index 00000000..480e115f --- /dev/null +++ b/src/Invex.Atom.Build.Analyzers/AT0003_ConfigureHostBuilderPartialMethodNotImplementedCodeFixProvider.cs @@ -0,0 +1,124 @@ +using Microsoft.CodeAnalysis.Formatting; + +namespace Invex.Atom.Build.Analyzers; + +// ReSharper disable once InconsistentNaming +/// +/// Provides a code fix for interfaces decorated with [ConfigureHostBuilder] that are missing +/// the required partial method implementation, by inserting the method stub. +/// +[ExportCodeFixProvider(LanguageNames.CSharp, + Name = nameof(AT0003_ConfigureHostBuilderPartialMethodNotImplementedCodeFixProvider))] +[Shared] +public class AT0003_ConfigureHostBuilderPartialMethodNotImplementedCodeFixProvider : CodeFixProvider +{ + private const string HostUsing = "Microsoft.Extensions.Hosting"; + + public sealed override ImmutableArray FixableDiagnosticIds { get; } = + [ + AT0003_ConfigureHostBuilderPartialMethodNotImplementedAnalyzer.DiagnosticId, + ]; + + public sealed override FixAllProvider GetFixAllProvider() => + WellKnownFixAllProviders.BatchFixer; + + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.First(); + var methodName = diagnostic.Properties["methodName"] ?? "ConfigureBuilder"; + + context.RegisterCodeFix(CodeAction.Create(string.Format(Resources.AT0003CodeFixTitle, methodName), + c => AddMethodImplementationAsync(context.Document, diagnostic, c), + nameof(Resources.AT0003CodeFixTitle)), + diagnostic); + + return Task.CompletedTask; + } + + private static async Task AddMethodImplementationAsync( + Document document, + Diagnostic diagnostic, + CancellationToken cancellationToken) + { + var root = await document + .GetSyntaxRootAsync(cancellationToken) + .ConfigureAwait(false); + + if (root == null) + return document; + + var methodName = diagnostic.Properties["methodName"] ?? "ConfigureBuilder"; + + var diagnosticSpan = diagnostic.Location.SourceSpan; + var node = root.FindNode(diagnosticSpan); + + // Find the interface declaration + var interfaceDeclaration = node.FirstAncestorOrSelf(); + + if (interfaceDeclaration == null) + return document; + + // Create the method declaration: + // protected static partial void ConfigureBuilderFrom{InterfaceName}(IHostApplicationBuilder builder) + // { + // } + var method = SyntaxFactory + .MethodDeclaration(SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), + SyntaxFactory.Identifier(methodName)) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.ProtectedKeyword), + SyntaxFactory.Token(SyntaxKind.StaticKeyword), + SyntaxFactory.Token(SyntaxKind.PartialKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(new[] + { + SyntaxFactory + .Parameter(SyntaxFactory.Identifier("builder")) + .WithType(SyntaxFactory.IdentifierName("IHostApplicationBuilder")), + }))) + .WithBody(SyntaxFactory.Block()) + .NormalizeWhitespace(); + + // Convert semicolon-terminated empty type to brace-delimited type + var originalInterfaceDeclaration = interfaceDeclaration; + + if (interfaceDeclaration.SemicolonToken != default) + interfaceDeclaration = interfaceDeclaration + .WithSemicolonToken(default) + .WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken)) + .WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken)); + + // Add the method to the interface + var newInterfaceDeclaration = interfaceDeclaration.AddMembers(method); + var newRoot = root.ReplaceNode(originalInterfaceDeclaration, newInterfaceDeclaration); + + // Add using directive if not present + if (newRoot is CompilationUnitSyntax compilationUnit && + compilationUnit.Usings.All(u => u.Name?.ToString() != HostUsing)) + { + var eol = root + .DescendantTrivia() + .Where(t => t.IsKind(SyntaxKind.EndOfLineTrivia)) + .Select(t => t.ToString()) + .FirstOrDefault() ?? + "\n"; + + var usingDirective = SyntaxFactory + .UsingDirective(SyntaxFactory.ParseName(HostUsing)) + .NormalizeWhitespace() + .WithTrailingTrivia(SyntaxFactory.EndOfLine(eol)); + + newRoot = compilationUnit.AddUsings(usingDirective); + } + + var result = document.WithSyntaxRoot(newRoot); + + var resultRoot = await result + .GetSyntaxRootAsync(cancellationToken) + .ConfigureAwait(false) ?? + throw new InvalidOperationException(); + + var formattedRoot = Formatter.Format(resultRoot.WithAdditionalAnnotations(Formatter.Annotation), + document.Project.Solution.Workspace); + + return result.WithSyntaxRoot(formattedRoot); + } +} diff --git a/DecSm.Atom.Analyzers/AnalyzerReleases.Shipped.md b/src/Invex.Atom.Build.Analyzers/AnalyzerReleases.Shipped.md similarity index 58% rename from DecSm.Atom.Analyzers/AnalyzerReleases.Shipped.md rename to src/Invex.Atom.Build.Analyzers/AnalyzerReleases.Shipped.md index 6da44a6c..b9640458 100644 --- a/DecSm.Atom.Analyzers/AnalyzerReleases.Shipped.md +++ b/src/Invex.Atom.Build.Analyzers/AnalyzerReleases.Shipped.md @@ -4,4 +4,6 @@ | Rule ID | Category | Severity | Notes | |---------|----------|----------|----------------------------------------------------------------------------------| -| AT0001 | Usage | Warning | TargetDefinition.RequiresParam() should not directly reference a Param property. | \ No newline at end of file +| AT0001 | Usage | Warning | TargetDefinition.RequiresParam() should not directly reference a Param property. | +| AT0002 | Usage | Warning | ConfigureHost partial method not implemented | +| AT0003 | Usage | Warning | ConfigureHostBuilder partial method not implemented | \ No newline at end of file diff --git a/src/Invex.Atom.Build.Analyzers/AnalyzerReleases.Unshipped.md b/src/Invex.Atom.Build.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..49520180 --- /dev/null +++ b/src/Invex.Atom.Build.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,4 @@ +### New Rules + +| Rule ID | Category | Severity | Notes | +|---------|----------|----------|----------------------------------------------------------| diff --git a/DecSm.Atom.Analyzers/DecSm.Atom.Analyzers.csproj b/src/Invex.Atom.Build.Analyzers/Invex.Atom.Build.Analyzers.csproj similarity index 91% rename from DecSm.Atom.Analyzers/DecSm.Atom.Analyzers.csproj rename to src/Invex.Atom.Build.Analyzers/Invex.Atom.Build.Analyzers.csproj index 5245509a..53d91dd8 100644 --- a/DecSm.Atom.Analyzers/DecSm.Atom.Analyzers.csproj +++ b/src/Invex.Atom.Build.Analyzers/Invex.Atom.Build.Analyzers.csproj @@ -9,12 +9,10 @@ true true - - DecSm.Atom.Analyzers - DecSm.Atom.Analyzers false false + Invex.Atom.Build.Analyzers diff --git a/DecSm.Atom.Analyzers/Properties/launchSettings.json b/src/Invex.Atom.Build.Analyzers/Properties/launchSettings.json similarity index 75% rename from DecSm.Atom.Analyzers/Properties/launchSettings.json rename to src/Invex.Atom.Build.Analyzers/Properties/launchSettings.json index 04492f04..d3111d0c 100644 --- a/DecSm.Atom.Analyzers/Properties/launchSettings.json +++ b/src/Invex.Atom.Build.Analyzers/Properties/launchSettings.json @@ -3,7 +3,7 @@ "profiles": { "DebugRoslynAnalyzers": { "commandName": "DebugRoslynComponent", - "targetProject": "../DecSm.Atom.Analyzers.Sample/DecSm.Atom.Analyzers.Sample.csproj" + "targetProject": "../Invex.Atom.Analyzers.Sample/Invex.Atom.Analyzers.Sample.csproj" } } } \ No newline at end of file diff --git a/DecSm.Atom.Analyzers/Resources.Designer.cs b/src/Invex.Atom.Build.Analyzers/Resources.Designer.cs similarity index 56% rename from DecSm.Atom.Analyzers/Resources.Designer.cs rename to src/Invex.Atom.Build.Analyzers/Resources.Designer.cs index e654bbfa..6109997a 100644 --- a/DecSm.Atom.Analyzers/Resources.Designer.cs +++ b/src/Invex.Atom.Build.Analyzers/Resources.Designer.cs @@ -7,7 +7,7 @@ // //------------------------------------------------------------------------------ -namespace DecSm.Atom.Analyzers { +namespace Invex.Atom.Build.Analyzers { using System; @@ -38,7 +38,7 @@ internal Resources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DecSm.Atom.Analyzers.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Invex.Atom.Build.Analyzers.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; @@ -94,5 +94,77 @@ internal static string AT0001Title { return ResourceManager.GetString("AT0001Title", resourceCulture); } } + + /// + /// Looks up a localized string similar to Implement '{0}'. + /// + internal static string AT0002CodeFixTitle { + get { + return ResourceManager.GetString("AT0002CodeFixTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Interfaces decorated with [ConfigureHost] must implement the generated partial method ConfigureHostFrom{InterfaceName}.. + /// + internal static string AT0002Description { + get { + return ResourceManager.GetString("AT0002Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Interface '{0}' is missing the required partial method implementation '{1}'. + /// + internal static string AT0002MessageFormat { + get { + return ResourceManager.GetString("AT0002MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ConfigureHost partial method not implemented. + /// + internal static string AT0002Title { + get { + return ResourceManager.GetString("AT0002Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Implement '{0}'. + /// + internal static string AT0003CodeFixTitle { + get { + return ResourceManager.GetString("AT0003CodeFixTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Interfaces decorated with [ConfigureHostBuilder] must implement the generated partial method ConfigureBuilderFrom{InterfaceName}.. + /// + internal static string AT0003Description { + get { + return ResourceManager.GetString("AT0003Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Interface '{0}' is missing the required partial method implementation '{1}'. + /// + internal static string AT0003MessageFormat { + get { + return ResourceManager.GetString("AT0003MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to ConfigureHostBuilder partial method not implemented. + /// + internal static string AT0003Title { + get { + return ResourceManager.GetString("AT0003Title", resourceCulture); + } + } } } diff --git a/src/Invex.Atom.Build.Analyzers/Resources.resx b/src/Invex.Atom.Build.Analyzers/Resources.resx new file mode 100644 index 00000000..a379ec9b --- /dev/null +++ b/src/Invex.Atom.Build.Analyzers/Resources.resx @@ -0,0 +1,76 @@ + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + TargetDefinition.RequiresParam() should not directly reference a Param property. + An optional longer localizable description of the diagnostic. + + + Parameter '{0}' should be wrapped with nameof operator + The format-able message the diagnostic displays. + + + RequiresParam should use nameof expression + The title of the diagnostic. + + + Replace with nameof({0}) + The title of the code fix. + + + Interfaces decorated with [ConfigureHost] must implement the generated partial method ConfigureHostFrom{InterfaceName}. + An optional longer localizable description of the diagnostic. + + + Interface '{0}' is missing the required partial method implementation '{1}' + The format-able message the diagnostic displays. + + + ConfigureHost partial method not implemented + The title of the diagnostic. + + + Implement '{0}' + The title of the code fix. + + + Interfaces decorated with [ConfigureHostBuilder] must implement the generated partial method ConfigureBuilderFrom{InterfaceName}. + An optional longer localizable description of the diagnostic. + + + Interface '{0}' is missing the required partial method implementation '{1}' + The format-able message the diagnostic displays. + + + ConfigureHostBuilder partial method not implemented + The title of the diagnostic. + + + Implement '{0}' + The title of the code fix. + + \ No newline at end of file diff --git a/DecSm.Atom.Analyzers/_usings.cs b/src/Invex.Atom.Build.Analyzers/_usings.cs similarity index 100% rename from DecSm.Atom.Analyzers/_usings.cs rename to src/Invex.Atom.Build.Analyzers/_usings.cs diff --git a/src/Invex.Atom.Build.SourceGenerators/BuildDefinitionInterfaceSourceGenerator.cs b/src/Invex.Atom.Build.SourceGenerators/BuildDefinitionInterfaceSourceGenerator.cs new file mode 100644 index 00000000..3aa8afe3 --- /dev/null +++ b/src/Invex.Atom.Build.SourceGenerators/BuildDefinitionInterfaceSourceGenerator.cs @@ -0,0 +1,481 @@ +// ReSharper disable ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator - Perf +// ReSharper disable ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - Perf + +namespace Invex.Atom.Build.SourceGenerators; + +[Generator] +public sealed class BuildDefinitionInterfaceSourceGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var classSymbols = context + .SyntaxProvider + .ForAttributeWithMetadataName(BuildDefinitionAttribute, + static (node, _) => node is InterfaceDeclarationSyntax, + static (context, _) => (INamedTypeSymbol)context.TargetSymbol) + .WithTrackingName(nameof(BuildDefinitionInterfaceSourceGenerator)); + + context.RegisterSourceOutput(classSymbols.Select(static (symbol, ct) => GeneratePartial(symbol, ct)), + static (context, data) => + { + if (data.SourceCode is not null) + context.AddSource($"{GetClassNameForInterface(data.InterfaceName)}.g.cs", + SourceText.From(data.SourceCode, Encoding.UTF8)); + }); + } + + private static InterfaceNameWithSourceCode GeneratePartial( + INamedTypeSymbol classSymbol, + CancellationToken cancellationToken) + { + var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + var isGlobalNamespace = namespaceName is ""; + + var allInterfaces = classSymbol.AllInterfaces; + var (allTargets, allParams) = GetTargetsAndParams(classSymbol, allInterfaces); + + var configureHostMethod = GetConfigureHostMethod(allInterfaces); + var configureHostBuilderMethod = GetConfigureBuilderMethod(allInterfaces); + + var inheritConfigureHostLine = configureHostMethod.Any || configureHostBuilderMethod.Any + ? ", Invex.Atom.Build.Hosting.IConfigureHost" + : string.Empty; + + var configureHostText = configureHostMethod.Any || configureHostBuilderMethod.Any + ? configureHostMethod.Text + : string.Empty; + + var configureHostBuilderText = configureHostMethod.Any || configureHostBuilderMethod.Any + ? configureHostBuilderMethod.Text + : string.Empty; + + cancellationToken.ThrowIfCancellationRequested(); + + var sourceCode = BuildSourceCode(isGlobalNamespace + ? string.Empty + : $"global using static {classSymbol.ToDisplayString()};", + isGlobalNamespace + ? string.Empty + : $"namespace {namespaceName};", + classSymbol.Name, + inheritConfigureHostLine, + [ + // new("Options", [GetOptionsOverride(classSymbol)]), + new("Targets", [GetTargetDefinitionsField(allTargets), GetTargetDefinitionsProperty(allTargets)]), + new("Params", + [ + GetParamDefinitionsField(allParams), + GetParamDefinitionsProperty(allParams), + GetParamsDataClass(allParams), + GetParamsField(), + GetAccessParamMethod(allParams), + ]), + new("Host", [configureHostText, configureHostBuilderText]), + ]); + + return new(classSymbol.Name, sourceCode); + } + + private static string BuildSourceCode( + string globalUsingStaticLine, + string namespaceLine, + string classNameSimple, + string configureHostInherit, + CodeRegion[] codeRegions) + { + var sb = new StringBuilder(); + + sb.AppendLine(""" + // + + #nullable enable + #pragma warning disable CS0169 + + """); + + sb.AppendLineIfNotBlank(globalUsingStaticLine, null, string.Empty); + + sb.AppendLine(""" + using System.Diagnostics.CodeAnalysis; + using System.Linq.Expressions; + using Microsoft.Extensions.DependencyInjection; + using Invex.Atom.Build.Definition; + using Invex.Atom.Build.Params; + + """); + + sb.AppendLineIfNotBlank(namespaceLine, null, string.Empty); + + sb.AppendLine($$""" + [JetBrains.Annotations.PublicAPI] + [System.Diagnostics.CodeAnalysis.SuppressMessage("ReSharper", "PossibleInterfaceMemberAmbiguity")] + internal sealed class {{GetClassNameForInterface(classNameSimple)}} : {{BuildDefinition}}, {{classNameSimple}}{{configureHostInherit}} + { + """); + + sb.AppendLine($$""" + public {{GetClassNameForInterface(classNameSimple)}}(System.IServiceProvider services) : base(services) { } + """); + + foreach (var codeRegion in codeRegions) + { + var appendRegion = false; + + foreach (var block in codeRegion.RegionBlocks) + if (block.Length > 0) + appendRegion = true; + + if (!appendRegion) + continue; + + sb.AppendLine(); + sb.AppendLine($" #region {codeRegion.RegionName}"); + + foreach (var fieldDeclaration in codeRegion.RegionBlocks) + sb.AppendLineIfNotBlank(fieldDeclaration, string.Empty); + + sb.AppendLine(); + sb.AppendLine($" #endregion {codeRegion.RegionName}"); + } + + sb.AppendLine("}"); + + return sb.ToString(); + } + + private static TargetsAndParams GetTargetsAndParams( + INamedTypeSymbol classSymbol, + ImmutableArray allInterfaces) + { + var targetsBuilder = ImmutableArray.CreateBuilder(64); + var paramsBuilder = ImmutableArray.CreateBuilder(64); + HashSet addedTargetNames = []; + + foreach (var memberSymbol in classSymbol.GetMembers()) + if (memberSymbol is IPropertySymbol propertySymbol) + { + if (propertySymbol.Type.ToDisplayString() is Target && addedTargetNames.Add(propertySymbol.Name)) + targetsBuilder.Add(new(propertySymbol, classSymbol)); + + foreach (var attributeData in propertySymbol.GetAttributes()) + { + if (attributeData.AttributeClass?.ToDisplayString() is not ParamDefinitionAttribute + and not SecretDefinitionAttribute) + continue; + + paramsBuilder.Add(new(propertySymbol, attributeData, classSymbol)); + + break; + } + } + + foreach (var interfaceSymbol in allInterfaces) + foreach (var memberSymbol in interfaceSymbol.GetMembers()) + if (memberSymbol is IPropertySymbol propertySymbol) + { + if (propertySymbol.Type.ToDisplayString() is Target && addedTargetNames.Add(propertySymbol.Name)) + targetsBuilder.Add(new(propertySymbol, interfaceSymbol)); + + foreach (var attributeData in propertySymbol.GetAttributes()) + { + if (attributeData.AttributeClass?.ToDisplayString() is not ParamDefinitionAttribute + and not SecretDefinitionAttribute) + continue; + + paramsBuilder.Add(new(propertySymbol, attributeData, interfaceSymbol)); + + break; + } + } + + return new(targetsBuilder.ToImmutable(), paramsBuilder.ToImmutable()); + } + + private static string GetTargetDefinitionsField(ImmutableArray allTargets) => + allTargets.IsEmpty + ? string.Empty + : $" private {IReadOnlyDictionary}? _targetDefinitions;"; + + private static string GetParamDefinitionsField(ImmutableArray allParams) + { + if (allParams.IsEmpty) + return string.Empty; + + var sb = new StringBuilder(); + + sb.AppendLine( + $" private readonly {IReadOnlyDictionary} _paramDefinitions = new {Dictionary}()"); + + sb.AppendLine(" {"); + + foreach (var param in allParams) + { + var nameofExpression = $"nameof({param.Type.ToDisplayString()}.{param.Property.Name})"; + + var argName = param + .Attribute + .ConstructorArguments[0] + .Value + ?.ToString(); + + var description = param + .Attribute + .ConstructorArguments[1] + .Value + ?.ToString(); + + var sources = + $"({param.Attribute.ConstructorArguments[2].Type?.ToDisplayString()}){param.Attribute.ConstructorArguments[2].Value}"; + + var isSecret = param.Attribute.AttributeClass?.ToDisplayString() is SecretDefinitionAttribute; + + var chainedParams = param.Attribute.ConstructorArguments.Length > 3 + ? param.Attribute.ConstructorArguments[3].Kind is TypedConstantKind.Array + ? param.Attribute.ConstructorArguments[3].Values + : [] + : []; + + sb.AppendLine(" {"); + sb.AppendLine($" {nameofExpression}, new({nameofExpression})"); + sb.AppendLine(" {"); + sb.AppendLine($""" ArgName = "{argName}","""); + sb.AppendLine($""" Description = "{description}","""); + sb.AppendLine($" Sources = {sources},"); + sb.AppendLine($" IsSecret = {isSecret.ToString().ToLower()},"); + + if (chainedParams.IsDefaultOrEmpty) + { + sb.AppendLine(" ChainedParams = [],"); + } + else + { + sb.Append(" ChainedParams = [ "); + + foreach (var v in chainedParams) + sb.Append($"\"{v.Value?.ToString() ?? string.Empty}\", "); + + sb.AppendLine("],"); + } + + sb.AppendLine(" }"); + sb.AppendLine(" },"); + } + + sb.AppendLine(" };"); + + return sb.ToString(); + } + + private static string GetParamsField() => + " private ParamsData? _params;"; + + private static string GetTargetDefinitionsProperty(ImmutableArray allTargets) + { + if (allTargets.IsEmpty) + return + $$""" public override {{IReadOnlyDictionary}} TargetDefinitions { get; } = new {{Dictionary}}();"""; + + var sb = new StringBuilder(); + + sb.AppendLine( + $" public override {IReadOnlyDictionary} TargetDefinitions => _targetDefinitions ??= new {Dictionary}"); + + sb.AppendLine(" {"); + + foreach (var target in allTargets) + sb.AppendLine( + $$""" { nameof({{target.Type.ToDisplayString()}}.{{target.Property.Name}}), (({{target.Type.ToDisplayString()}})this).{{target.Property.Name}} },"""); + + sb.AppendLine(" };"); + + return sb.ToString(); + } + + private static string GetParamDefinitionsProperty(ImmutableArray allParams) => + allParams.IsEmpty + ? $$""" public override {{IReadOnlyDictionary}} ParamDefinitions { get; } = new {{Dictionary}}();""" + : $" public override {IReadOnlyDictionary} ParamDefinitions => _paramDefinitions;"; + + // private static string GetWorkflowTargetsClass(ImmutableArray allTargets) + // { + // if (allTargets.IsEmpty) + // return string.Empty; + // + // var sb = new StringBuilder(); + // sb.AppendLine(" public static class WorkflowTargets"); + // sb.AppendLine(" {"); + // + // foreach (var target in allTargets) + // sb.AppendLine( + // $" public static readonly {WorkflowTargetDefinition} {target.Property.Name} = new(nameof({target.Type.ToDisplayString()}.{target.Property.Name}));"); + // + // sb.AppendLine(" }"); + // + // return sb.ToString(); + // } + + // private static string GetWorkflowTargetMethod(ImmutableArray allTargets) + // { + // if (allTargets.IsEmpty) + // return + // $$"""private static {{WorkflowTargetDefinition}} Target(string name) => throw new System.ArgumentException($"Target with name '{name}' is not defined in the build definition.", nameof(name));"""; + // + // var sb = new StringBuilder(); + // + // sb.AppendLine($" private static {WorkflowTargetDefinition} WorkflowTarget(string name) =>"); + // sb.AppendLine(" name switch"); + // sb.AppendLine(" {"); + // + // foreach (var target in allTargets) + // sb.AppendLine( + // $" nameof({target.Type.ToDisplayString()}.{target.Property.Name}) => WorkflowTargets.{target.Property.Name},"); + // + // sb.AppendLine( + // """ _ => throw new System.ArgumentException($"Target with name '{name}' is not defined in the build definition.", nameof(name))"""); + // + // sb.AppendLine(" };"); + // + // return sb.ToString(); + // } + + private static string GetParamsDataClass(ImmutableArray allParams) + { + if (allParams.IsEmpty) + return " public sealed class ParamsData() { }"; + + var sb = new StringBuilder(); + sb.AppendLine($" public sealed class ParamsData({IBuildDefinition} buildDefinition)"); + sb.AppendLine(" {"); + + foreach (var param in allParams) + sb.AppendLine( + $" public ParamDefinition {param.Property.Name} => buildDefinition.ParamDefinitions[nameof({param.Type.ToDisplayString()}.{param.Property.Name})];"); + + sb.AppendLine(" }"); + + return sb.ToString(); + } + + // private static string GetWorkflowParamsProperty(ImmutableArray allParams) => + // allParams.IsEmpty + // ? " public ParamsData WorkflowParams => _params ??= new();" + // : " public ParamsData WorkflowParams => _params ??= new(this);"; + + private static string GetAccessParamMethod(ImmutableArray allParams) + { + if (allParams.IsEmpty) + return + """ public override object? AccessParam(string paramName) => throw new System.ArgumentException($"Param with name '{paramName}' is not defined in the build definition.", nameof(paramName));"""; + + var sb = new StringBuilder(); + + sb.AppendLine(" public override object? AccessParam(string paramName) =>"); + sb.AppendLine(" paramName switch"); + sb.AppendLine(" {"); + + foreach (var param in allParams) + sb.AppendLine( + $" nameof({param.Type.ToDisplayString()}.{param.Property.Name}) => (({param.Type.ToDisplayString()})this).{param.Property.Name},"); + + sb.AppendLine( + """ _ => throw new System.ArgumentException($"Param with name '{paramName}' is not defined in the build definition.", nameof(paramName))"""); + + sb.AppendLine(" };"); + + return sb.ToString(); + } + + private static TextResult GetConfigureHostMethod(ImmutableArray allInterfaces) + { + var configureHostInterfaces = new List(64); + + foreach (var interfaceSymbol in allInterfaces) + foreach (var attributeData in interfaceSymbol.GetAttributes()) + { + if (attributeData.AttributeClass?.ToDisplayString() is not ConfigureHostAttribute) + continue; + + configureHostInterfaces.Add(interfaceSymbol); + + break; + } + + if (configureHostInterfaces.Count is 0) + return new(" public void ConfigureBuildHost(Microsoft.Extensions.Hosting.IHost builder) { }", false); + + var sb = new StringBuilder(); + + sb.AppendLine(" public void ConfigureBuildHost(Microsoft.Extensions.Hosting.IHost builder)"); + sb.AppendLine(" {"); + + foreach (var interfaceSymbol in configureHostInterfaces) + sb.AppendLine( + $" {interfaceSymbol.ToDisplayString()}.ConfigureHostFrom{interfaceSymbol.Name}(builder);"); + + sb.AppendLine(" }"); + + return new(sb.ToString(), true); + } + + private static TextResult GetConfigureBuilderMethod(ImmutableArray allInterfaces) + { + var configureHostInterfaces = new List(64); + var registerHostInterfaces = new List(64); + + foreach (var interfaceSymbol in allInterfaces) + { + foreach (var subInterfaceSymbol in interfaceSymbol.AllInterfaces) + { + if (subInterfaceSymbol.ToDisplayString() is not IBuildDefinition and not IBuildAccessor) + continue; + + registerHostInterfaces.Add(interfaceSymbol); + + break; + } + + foreach (var attributeData in interfaceSymbol.GetAttributes()) + { + if (attributeData.AttributeClass?.ToDisplayString() is not ConfigureHostBuilderAttribute) + continue; + + configureHostInterfaces.Add(interfaceSymbol); + + break; + } + } + + if (configureHostInterfaces.Count + registerHostInterfaces.Count is 0) + return new( + " public void ConfigureBuildHostBuilder(Microsoft.Extensions.Hosting.IHostApplicationBuilder builder) { }", + false); + + var sb = new StringBuilder(); + + sb.AppendLine( + " public void ConfigureBuildHostBuilder(Microsoft.Extensions.Hosting.IHostApplicationBuilder builder)"); + + sb.AppendLine(" {"); + + foreach (var interfaceSymbol in registerHostInterfaces) + { + var interfaceFullName = interfaceSymbol.ToDisplayString(); + + sb.AppendLine( + $" Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton<{interfaceFullName}>(builder.Services, static p => ({interfaceFullName})p.GetRequiredService<{IBuildDefinition}>());"); + } + + foreach (var interfaceSymbol in configureHostInterfaces) + sb.AppendLine( + $" {interfaceSymbol.ToDisplayString()}.ConfigureBuilderFrom{interfaceSymbol.Name}(builder);"); + + sb.AppendLine(" }"); + + return new(sb.ToString(), true); + } + + private static string GetClassNameForInterface(string interfaceName) => + interfaceName.Length > 1 && interfaceName[0] is 'I' + ? interfaceName.Substring(1) + : $"{interfaceName}Impl"; +} diff --git a/DecSm.Atom.SourceGenerators/BuildDefinitionSourceGenerator.cs b/src/Invex.Atom.Build.SourceGenerators/BuildDefinitionSourceGenerator.cs similarity index 86% rename from DecSm.Atom.SourceGenerators/BuildDefinitionSourceGenerator.cs rename to src/Invex.Atom.Build.SourceGenerators/BuildDefinitionSourceGenerator.cs index 5526505c..01ca509e 100644 --- a/DecSm.Atom.SourceGenerators/BuildDefinitionSourceGenerator.cs +++ b/src/Invex.Atom.Build.SourceGenerators/BuildDefinitionSourceGenerator.cs @@ -1,7 +1,7 @@ // ReSharper disable ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator - Perf // ReSharper disable ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - Perf -namespace DecSm.Atom.SourceGenerators; +namespace Invex.Atom.Build.SourceGenerators; [Generator] public sealed class BuildDefinitionSourceGenerator : IIncrementalGenerator @@ -13,7 +13,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .ForAttributeWithMetadataName(BuildDefinitionAttribute, static (node, _) => node is ClassDeclarationSyntax, static (context, _) => (INamedTypeSymbol)context.TargetSymbol) - .WithTrackingName(nameof(GenerateInterfaceMembersSourceGenerator)); + .WithTrackingName(nameof(BuildDefinitionSourceGenerator)); context.RegisterSourceOutput(classSymbols.Select(static (symbol, ct) => GeneratePartial(symbol, ct)), static (context, data) => @@ -37,7 +37,7 @@ private static ClassNameWithSourceCode GeneratePartial( var configureHostBuilderMethod = GetConfigureBuilderMethod(allInterfaces); var inheritConfigureHostLine = configureHostMethod.Any || configureHostBuilderMethod.Any - ? ", DecSm.Atom.Hosting.IConfigureHost" + ? ", Invex.Atom.Build.Hosting.IConfigureHost" : string.Empty; var configureHostText = configureHostMethod.Any || configureHostBuilderMethod.Any @@ -60,20 +60,13 @@ private static ClassNameWithSourceCode GeneratePartial( classSymbol.ToDisplayString(), inheritConfigureHostLine, [ - new("Targets", - [ - GetTargetDefinitionsField(allTargets), - GetTargetDefinitionsProperty(allTargets), - GetWorkflowTargetsClass(allTargets), - GetWorkflowTargetMethod(allTargets), - ]), + new("Targets", [GetTargetDefinitionsField(allTargets), GetTargetDefinitionsProperty(allTargets)]), new("Params", [ GetParamDefinitionsField(allParams), GetParamDefinitionsProperty(allParams), GetParamsDataClass(allParams), GetParamsField(), - GetParamsProperty(allParams), GetAccessParamMethod(allParams), ]), new("Host", [configureHostText, configureHostBuilderText]), @@ -96,6 +89,7 @@ private static string BuildSourceCode( // #nullable enable + #pragma warning disable CS0169 """); @@ -106,10 +100,10 @@ private static string BuildSourceCode( using System.Linq.Expressions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; - using DecSm.Atom.Build.Definition; - using DecSm.Atom.Params; - using DecSm.Atom.Paths; - using DecSm.Atom.Process; + using Invex.Atom.Build.Definition; + using Invex.Atom.Build.Params; + using Invex.FileSystem; + using Invex.Process; """); @@ -126,7 +120,7 @@ partial class {{classNameSimple}} : {{IBuildDefinition}}{{configureHostInherit}} private ILogger Logger => Services.GetRequiredService().CreateLogger("{{classNameFull}}"); - private IAtomFileSystem FileSystem => GetService(); + private IRootedFileSystem FileSystem => GetService(); private IProcessRunner ProcessRunner => GetService(); @@ -331,47 +325,47 @@ private static string GetParamDefinitionsProperty(ImmutableArray allP ? $$""" public override {{IReadOnlyDictionary}} ParamDefinitions { get; } = new {{Dictionary}}();""" : $" public override {IReadOnlyDictionary} ParamDefinitions => _paramDefinitions;"; - private static string GetWorkflowTargetsClass(ImmutableArray allTargets) - { - if (allTargets.IsEmpty) - return string.Empty; - - var sb = new StringBuilder(); - sb.AppendLine(" public static class WorkflowTargets"); - sb.AppendLine(" {"); - - foreach (var target in allTargets) - sb.AppendLine( - $" public static readonly {WorkflowTargetDefinition} {target.Property.Name} = new(nameof({target.Type.ToDisplayString()}.{target.Property.Name}));"); - - sb.AppendLine(" }"); - - return sb.ToString(); - } - - private static string GetWorkflowTargetMethod(ImmutableArray allTargets) - { - if (allTargets.IsEmpty) - return - $$"""private static {{WorkflowTargetDefinition}} Target(string name) => throw new System.ArgumentException($"Target with name '{name}' is not defined in the build definition.", nameof(name));"""; - - var sb = new StringBuilder(); - - sb.AppendLine($" private static {WorkflowTargetDefinition} WorkflowTarget(string name) =>"); - sb.AppendLine(" name switch"); - sb.AppendLine(" {"); - - foreach (var target in allTargets) - sb.AppendLine( - $" nameof({target.Type.ToDisplayString()}.{target.Property.Name}) => WorkflowTargets.{target.Property.Name},"); - - sb.AppendLine( - """ _ => throw new System.ArgumentException($"Target with name '{name}' is not defined in the build definition.", nameof(name))"""); - - sb.AppendLine(" };"); - - return sb.ToString(); - } + // private static string GetWorkflowTargetsClass(ImmutableArray allTargets) + // { + // if (allTargets.IsEmpty) + // return string.Empty; + // + // var sb = new StringBuilder(); + // sb.AppendLine(" public static class WorkflowTargets"); + // sb.AppendLine(" {"); + // + // foreach (var target in allTargets) + // sb.AppendLine( + // $" public static readonly {WorkflowTargetDefinition} {target.Property.Name} = new(nameof({target.Type.ToDisplayString()}.{target.Property.Name}));"); + // + // sb.AppendLine(" }"); + // + // return sb.ToString(); + // } + + // private static string GetWorkflowTargetMethod(ImmutableArray allTargets) + // { + // if (allTargets.IsEmpty) + // return + // $$"""private static {{WorkflowTargetDefinition}} Target(string name) => throw new System.ArgumentException($"Target with name '{name}' is not defined in the build definition.", nameof(name));"""; + // + // var sb = new StringBuilder(); + // + // sb.AppendLine($" private static {WorkflowTargetDefinition} WorkflowTarget(string name) =>"); + // sb.AppendLine(" name switch"); + // sb.AppendLine(" {"); + // + // foreach (var target in allTargets) + // sb.AppendLine( + // $" nameof({target.Type.ToDisplayString()}.{target.Property.Name}) => WorkflowTargets.{target.Property.Name},"); + // + // sb.AppendLine( + // """ _ => throw new System.ArgumentException($"Target with name '{name}' is not defined in the build definition.", nameof(name))"""); + // + // sb.AppendLine(" };"); + // + // return sb.ToString(); + // } private static string GetParamsDataClass(ImmutableArray allParams) { @@ -391,10 +385,10 @@ private static string GetParamsDataClass(ImmutableArray allParams) return sb.ToString(); } - private static string GetParamsProperty(ImmutableArray allParams) => - allParams.IsEmpty - ? " public ParamsData Params => _params ??= new();" - : " public ParamsData Params => _params ??= new(this);"; + // private static string GetWorkflowParamsProperty(ImmutableArray allParams) => + // allParams.IsEmpty + // ? " public ParamsData WorkflowParams => _params ??= new();" + // : " public ParamsData WorkflowParams => _params ??= new(this);"; private static string GetAccessParamMethod(ImmutableArray allParams) { @@ -444,7 +438,8 @@ private static TextResult GetConfigureHostMethod(ImmutableArray node is ClassDeclarationSyntax, + static (node, _) => node is ClassDeclarationSyntax or InterfaceDeclarationSyntax, static (attrContext, _) => (INamedTypeSymbol)attrContext.TargetSymbol) .WithTrackingName(nameof(GenerateEntryPointSourceGenerator)); @@ -23,12 +23,23 @@ private static void GenerateCode(SourceProductionContext context, ImmutableArray private static void GeneratePartial(SourceProductionContext context, INamedTypeSymbol classSymbol) { + var typeName = classSymbol.ToDisplayString(); + + // If it's an interface, we need to trim the "I" prefix from the last part + var typeParts = typeName.Split('.'); + var lastPart = typeParts[typeParts.Length - 1]; + + if (lastPart.StartsWith("I") && lastPart.Length > 1 && char.IsUpper(lastPart[1])) + typeParts[typeParts.Length - 1] = lastPart.Substring(1); + + var trimmedTypeName = string.Join(".", typeParts); + var code = $""" // - DecSm.Atom.Hosting.AtomHost.Run<{classSymbol.ToDisplayString()}>(args); + Invex.Atom.Build.Hosting.AtomHost.Run<{trimmedTypeName}>(args); """; - context.AddSource($"{classSymbol.Name}.g.cs", SourceText.From(code, Encoding.UTF8)); + context.AddSource($"{classSymbol.Name.TrimStart('I')}.g.cs", SourceText.From(code, Encoding.UTF8)); } } diff --git a/DecSm.Atom.SourceGenerators/GenerateInterfaceMembersSourceGenerator.cs b/src/Invex.Atom.Build.SourceGenerators/GenerateInterfaceMembersSourceGenerator.cs similarity index 99% rename from DecSm.Atom.SourceGenerators/GenerateInterfaceMembersSourceGenerator.cs rename to src/Invex.Atom.Build.SourceGenerators/GenerateInterfaceMembersSourceGenerator.cs index cef396cc..d863fbec 100644 --- a/DecSm.Atom.SourceGenerators/GenerateInterfaceMembersSourceGenerator.cs +++ b/src/Invex.Atom.Build.SourceGenerators/GenerateInterfaceMembersSourceGenerator.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.SourceGenerators; +namespace Invex.Atom.Build.SourceGenerators; [Generator] public class GenerateInterfaceMembersSourceGenerator : IIncrementalGenerator diff --git a/DecSm.Atom.SourceGenerators/GenerateSolutionModelSourceGenerator.cs b/src/Invex.Atom.Build.SourceGenerators/GenerateSolutionModelSourceGenerator.cs similarity index 90% rename from DecSm.Atom.SourceGenerators/GenerateSolutionModelSourceGenerator.cs rename to src/Invex.Atom.Build.SourceGenerators/GenerateSolutionModelSourceGenerator.cs index ef8fd79e..b978351c 100644 --- a/DecSm.Atom.SourceGenerators/GenerateSolutionModelSourceGenerator.cs +++ b/src/Invex.Atom.Build.SourceGenerators/GenerateSolutionModelSourceGenerator.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.SourceGenerators; +namespace Invex.Atom.Build.SourceGenerators; [Generator] public class GenerateSolutionModelSourceGenerator : IIncrementalGenerator @@ -13,7 +13,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var classSymbolsProvider = context .SyntaxProvider .ForAttributeWithMetadataName(GenerateSolutionModelAttribute, - static (node, _) => node is ClassDeclarationSyntax, + static (node, _) => node is ClassDeclarationSyntax or InterfaceDeclarationSyntax, static (ctx, _) => (INamedTypeSymbol)ctx.TargetSymbol) .WithTrackingName(nameof(GenerateSolutionModelSourceGenerator)) .Collect(); @@ -174,15 +174,15 @@ private static void GeneratePartial( /// /// {{projectPath}} /// - public interface {{validIdentifier}} : IPathLocator + public interface {{validIdentifier}} : IPathMarker { public const string Name = @"{{kvp.Key}}"; - static RootedPath IPathLocator.Path(IAtomFileSystem fileSystem) => - fileSystem.CreateRootedPath(@"{{projectPath}}"); + static RootedPath IPathMarker.Path(IFileSystem fileSystem) => + new RootedPath(fileSystem, @"{{projectPath}}"); - new static RootedPath Path(IAtomFileSystem fileSystem) => - fileSystem.CreateRootedPath(@"{{projectPath}}"); + new static RootedPath Path(IFileSystem fileSystem) => + new RootedPath(fileSystem, @"{{projectPath}}"); } """; })); @@ -195,19 +195,20 @@ static RootedPath IPathLocator.Path(IAtomFileSystem fileSystem) => #nullable enable - using DecSm.Atom.Paths; + using Invex.FileSystem; + using System.IO.Abstractions; {{namespaceLine}} /// /// {{solutionPathNormalized}} /// - public interface Solution : IPathLocator + public interface Solution : IPathMarker { public const string Name = @"{{solutionName}}"; - static RootedPath IPathLocator.Path(IAtomFileSystem fileSystem) => - fileSystem.CreateRootedPath(@"{{solutionPathNormalized}}"); + static RootedPath IPathMarker.Path(IFileSystem fileSystem) => + new RootedPath(fileSystem, @"{{solutionPathNormalized}}"); } public static class Projects diff --git a/DecSm.Atom.SourceGenerators/DecSm.Atom.SourceGenerators.csproj b/src/Invex.Atom.Build.SourceGenerators/Invex.Atom.Build.SourceGenerators.csproj similarity index 92% rename from DecSm.Atom.SourceGenerators/DecSm.Atom.SourceGenerators.csproj rename to src/Invex.Atom.Build.SourceGenerators/Invex.Atom.Build.SourceGenerators.csproj index e2d9dbc2..551499a4 100644 --- a/DecSm.Atom.SourceGenerators/DecSm.Atom.SourceGenerators.csproj +++ b/src/Invex.Atom.Build.SourceGenerators/Invex.Atom.Build.SourceGenerators.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/DecSm.Atom.SourceGenerators/SetupSourceGenerator.cs b/src/Invex.Atom.Build.SourceGenerators/SetupSourceGenerator.cs similarity index 83% rename from DecSm.Atom.SourceGenerators/SetupSourceGenerator.cs rename to src/Invex.Atom.Build.SourceGenerators/SetupSourceGenerator.cs index d00ee1f2..58ed536b 100644 --- a/DecSm.Atom.SourceGenerators/SetupSourceGenerator.cs +++ b/src/Invex.Atom.Build.SourceGenerators/SetupSourceGenerator.cs @@ -2,13 +2,13 @@ (Microsoft.CodeAnalysis.CSharp.Syntax.InterfaceDeclarationSyntax Declaration, bool HasConfigureBuilder, bool HasConfigureHost); -namespace DecSm.Atom.SourceGenerators; +namespace Invex.Atom.Build.SourceGenerators; [Generator] public class SetupSourceGenerator : IIncrementalGenerator { - private const string ConfigureHostBuilderAttributeFull = "DecSm.Atom.Hosting.ConfigureHostBuilderAttribute"; - private const string ConfigureHostAttributeFull = "DecSm.Atom.Hosting.ConfigureHostAttribute"; + private const string ConfigureHostBuilderAttributeFull = "Invex.Atom.Build.Hosting.ConfigureHostBuilderAttribute"; + private const string ConfigureHostAttributeFull = "Invex.Atom.Build.Hosting.ConfigureHostAttribute"; public void Initialize(IncrementalGeneratorInitializationContext context) => context.RegisterSourceOutput(context.CompilationProvider.Combine(context @@ -93,25 +93,17 @@ private static void GeneratePartial( .GetAttributes() .Any(attr => attr.AttributeClass?.ToDisplayString() == ConfigureHostAttributeFull); - var inheritsConfigureBuilder = interfaceSymbol.AllInterfaces.Any(i => i - .GetAttributes() - .Any(attr => attr.AttributeClass?.ToDisplayString() == ConfigureHostBuilderAttributeFull)); - - var inheritsConfigureHost = interfaceSymbol.AllInterfaces.Any(i => i - .GetAttributes() - .Any(attr => attr.AttributeClass?.ToDisplayString() == ConfigureHostAttributeFull)); - var setupBuilderLine = hasConfigureBuilder ? $""" [JetBrains.Annotations.UsedImplicitly] - protected {(inheritsConfigureBuilder ? "new " : string.Empty)}static partial void ConfigureBuilder(IHostApplicationBuilder builder); + protected static partial void ConfigureBuilderFrom{@interface}(IHostApplicationBuilder builder); """ : string.Empty; var setupHostLine = hasConfigureHost ? $""" [JetBrains.Annotations.UsedImplicitly] - protected {(inheritsConfigureHost ? "new " : string.Empty)}static partial void ConfigureHost(IHost host); + protected static partial void ConfigureHostFrom{@interface}(IHost host); """ : string.Empty; diff --git a/DecSm.Atom.SourceGenerators/Symbols.cs b/src/Invex.Atom.Build.SourceGenerators/Symbols.cs similarity index 60% rename from DecSm.Atom.SourceGenerators/Symbols.cs rename to src/Invex.Atom.Build.SourceGenerators/Symbols.cs index 5e178b18..a9b024a0 100644 --- a/DecSm.Atom.SourceGenerators/Symbols.cs +++ b/src/Invex.Atom.Build.SourceGenerators/Symbols.cs @@ -1,28 +1,33 @@ -namespace DecSm.Atom.SourceGenerators; +namespace Invex.Atom.Build.SourceGenerators; public static class Symbols { - public const string BuildDefinitionAttribute = "DecSm.Atom.Build.Definition.BuildDefinitionAttribute"; + public const string BuildDefinitionAttribute = "Invex.Atom.Build.Definition.BuildDefinitionAttribute"; - public const string GenerateEntryPointAttribute = "DecSm.Atom.Hosting.GenerateEntryPointAttribute"; + public const string BuildDefinitionInterfaceAttribute = + "Invex.Atom.Build.Definition.BuildDefinitionInterfaceAttribute"; + + public const string GenerateEntryPointAttribute = "Invex.Atom.Build.Hosting.GenerateEntryPointAttribute"; public const string GenerateInterfaceMembersAttribute = - "DecSm.Atom.Build.Definition.GenerateInterfaceMembersAttribute"; + "Invex.Atom.Build.Definition.GenerateInterfaceMembersAttribute"; + + public const string GenerateSolutionModelAttribute = "Invex.Atom.Build.Definition.GenerateSolutionModelAttribute"; - public const string GenerateSolutionModelAttribute = "DecSm.Atom.Build.Definition.GenerateSolutionModelAttribute"; + public const string BuildDefinition = "Invex.Atom.Build.Definition.BuildDefinition"; + public const string IBuildDefinition = "Invex.Atom.Build.Definition.IBuildDefinition"; + public const string IBuildAccessor = "Invex.Atom.Build.IBuildAccessor"; - public const string IBuildDefinition = "DecSm.Atom.Build.Definition.IBuildDefinition"; - public const string IBuildAccessor = "DecSm.Atom.Build.IBuildAccessor"; + public const string Target = "Invex.Atom.Build.Definition.Target"; - public const string Target = "DecSm.Atom.Build.Definition.Target"; - public const string WorkflowTargetDefinition = "DecSm.Atom.Workflows.Definition.WorkflowTargetDefinition"; + public const string ParamDefinition = "Invex.Atom.Build.Params.ParamDefinition"; + public const string ParamDefinitionAttribute = "Invex.Atom.Build.Params.ParamDefinitionAttribute"; + public const string SecretDefinitionAttribute = "Invex.Atom.Build.Params.SecretDefinitionAttribute"; - public const string ParamDefinition = "DecSm.Atom.Params.ParamDefinition"; - public const string ParamDefinitionAttribute = "DecSm.Atom.Params.ParamDefinitionAttribute"; - public const string SecretDefinitionAttribute = "DecSm.Atom.Params.SecretDefinitionAttribute"; + public const string ConfigureHostAttribute = "Invex.Atom.Build.Hosting.ConfigureHostAttribute"; + public const string ConfigureHostBuilderAttribute = "Invex.Atom.Build.Hosting.ConfigureHostBuilderAttribute"; - public const string ConfigureHostAttribute = "DecSm.Atom.Hosting.ConfigureHostAttribute"; - public const string ConfigureHostBuilderAttribute = "DecSm.Atom.Hosting.ConfigureHostBuilderAttribute"; + public const string IBuildOption = "Invex.Atom.Build.BuildOptions.IBuildOption"; public const string IReadOnlyDictionary = "System.Collections.Generic.IReadOnlyDictionary"; public const string Dictionary = "System.Collections.Generic.Dictionary"; @@ -35,6 +40,13 @@ public readonly record struct ClassNameWithSourceCode(string ClassName, string? public string? SourceCode { get; } = SourceCode; } +public readonly record struct InterfaceNameWithSourceCode(string InterfaceName, string? SourceCode) +{ + public string InterfaceName { get; } = InterfaceName; + + public string? SourceCode { get; } = SourceCode; +} + public readonly record struct CodeRegion(string? RegionName, string[] RegionBlocks) { public string? RegionName { get; } = RegionName; diff --git a/DecSm.Atom.SourceGenerators/_usings.cs b/src/Invex.Atom.Build.SourceGenerators/_usings.cs similarity index 82% rename from DecSm.Atom.SourceGenerators/_usings.cs rename to src/Invex.Atom.Build.SourceGenerators/_usings.cs index 47f9ab56..8b8a1215 100644 --- a/DecSm.Atom.SourceGenerators/_usings.cs +++ b/src/Invex.Atom.Build.SourceGenerators/_usings.cs @@ -6,4 +6,4 @@ global using Microsoft.CodeAnalysis; global using Microsoft.CodeAnalysis.CSharp.Syntax; global using Microsoft.CodeAnalysis.Text; -global using static DecSm.Atom.SourceGenerators.Symbols; +global using static Invex.Atom.Build.SourceGenerators.Symbols; diff --git a/DecSm.Atom/Args/IArg.cs b/src/Invex.Atom.Build/Args/Args.cs similarity index 89% rename from DecSm.Atom/Args/IArg.cs rename to src/Invex.Atom.Build/Args/Args.cs index 9968e750..7ea1b31d 100644 --- a/DecSm.Atom/Args/IArg.cs +++ b/src/Invex.Atom.Build/Args/Args.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Args; +namespace Invex.Atom.Build.Args; /// /// Defines the base interface for a parsed command-line argument. @@ -15,22 +15,12 @@ public interface IArg; /// The name of the build target to execute. This is case-insensitive. /// /// The must correspond to a target defined in the -/// . +/// . /// /// For the command atom MyTarget, a with `Name = "MyTarget"` is created. [PublicAPI] public sealed record CommandArg(string Name) : IArg; -/// -/// Represents the argument to generate or regenerate workflow files (e.g., for CI/CD systems). -/// -/// -/// This is triggered by the -g or --gen flag. -/// -/// For the command atom --gen, a is created. -[PublicAPI] -public sealed record GenArg : IArg; - /// /// Represents the argument to display help information. /// @@ -50,7 +40,7 @@ public sealed record HelpArg : IArg; /// The value provided for the parameter. /// /// The must match a parameter defined in the -/// . +/// . /// /// /// For the command atom Build --configuration Release, a would be created where diff --git a/DecSm.Atom/Args/CommandLineArgs.cs b/src/Invex.Atom.Build/Args/CommandLineArgs.cs similarity index 68% rename from DecSm.Atom/Args/CommandLineArgs.cs rename to src/Invex.Atom.Build/Args/CommandLineArgs.cs index 4cd1b887..cb7df76e 100644 --- a/DecSm.Atom/Args/CommandLineArgs.cs +++ b/src/Invex.Atom.Build/Args/CommandLineArgs.cs @@ -1,25 +1,53 @@ -namespace DecSm.Atom.Args; +namespace Invex.Atom.Build.Args; /// /// Contains the parsed command-line arguments for an Atom build execution, providing a structured representation of /// user input. /// -/// -/// A value indicating whether the command-line arguments were parsed successfully and are valid. If -/// false, the Atom application will typically terminate or display help. -/// -/// -/// A read-only list of objects representing the individual arguments parsed from the command -/// line. This list provides access to raw argument objects like , , or -/// . -/// /// /// An instance of this record is created by the after parsing the raw string /// arguments provided to the Atom application. /// [PublicAPI] -public sealed record CommandLineArgs(bool IsValid, IReadOnlyList Args) +public sealed record CommandLineArgs { + /// + /// Contains the parsed command-line arguments for an Atom build execution, providing a structured representation of + /// user input. + /// + /// + /// A value indicating whether the command-line arguments were parsed successfully and are valid. If + /// false, the Atom application will typically terminate or display help. + /// + /// + /// A read-only list of objects representing the individual arguments parsed from the command + /// line. This list provides access to raw argument objects like , , or + /// . + /// + /// + /// An instance of this record is created by the after parsing the raw string + /// arguments provided to the Atom application. + /// + public CommandLineArgs(bool IsValid, IEnumerable Args) + { + this.IsValid = IsValid; + + this.Args = Args.ToList(); + } + + /// + /// A value indicating whether the command-line arguments were parsed successfully and are valid. If + /// false, the Atom application will typically terminate or display help. + /// + public bool IsValid { get; init; } + + /// + /// A read-only list of objects representing the individual arguments parsed from the command + /// line. This list provides access to raw argument objects like , , or + /// . + /// + public IReadOnlyList Args { get; init; } + /// /// Gets a value indicating whether the help argument (-h or --help) was provided. /// @@ -29,21 +57,9 @@ public sealed record CommandLineArgs(bool IsValid, IReadOnlyList Args) /// Running atom --help or atom -h will result in this property being true. /// /// - /// + /// public bool HasHelp => Args.Any(arg => arg is HelpArg); - /// - /// Gets a value indicating whether the generate argument (-g or --gen) was provided. - /// - /// true if the generate argument is present; otherwise, false. - /// - /// This flag instructs the Atom framework to (re)generate workflow files (e.g., for CI/CD systems). - /// Running atom --gen or atom -g will result in this property being true. - /// - /// - /// - public bool HasGen => Args.Any(arg => arg is GenArg); - /// /// Gets a value indicating whether the skip argument (-s or --skip) was provided. /// @@ -53,7 +69,7 @@ public sealed record CommandLineArgs(bool IsValid, IReadOnlyList Args) /// Running atom MyTarget --skip or atom MyTarget -s will result in this property being true. /// /// - /// + /// public bool HasSkip => Args.Any(arg => arg is SkipArg); /// @@ -76,7 +92,7 @@ public sealed record CommandLineArgs(bool IsValid, IReadOnlyList Args) /// Running atom MyCommand --verbose or atom MyCommand -v will result in this property being true. /// /// - /// + /// public bool HasVerbose => Args.Any(arg => arg is VerboseArg); /// @@ -100,7 +116,7 @@ public sealed record CommandLineArgs(bool IsValid, IReadOnlyList Args) /// Running atom MyCommand -i will result in this property being true. /// /// - /// + /// public bool HasInteractive => Args.Any(arg => arg is InteractiveArg); /// @@ -148,4 +164,26 @@ public sealed record CommandLineArgs(bool IsValid, IReadOnlyList Args) .Select(arg => arg.ProjectName) .FirstOrDefault() ?? "_atom"; + + /// + /// Gets a list of validation errors for the current command-line arguments. + /// + /// A read-only list of error messages. An empty list indicates no validation errors. + public IReadOnlyList GetValidationErrors() + { + var errors = new List(); + + if (!IsValid) + errors.Add("One or more arguments could not be parsed"); + + errors.AddRange(Commands + .Where(command => string.IsNullOrWhiteSpace(command.Name)) + .Select(_ => "Target name cannot be empty")); + + errors.AddRange(Params + .Where(param => string.IsNullOrWhiteSpace(param.ParamName)) + .Select(param => $"Parameter name cannot be empty for value '{param.ParamValue}'")); + + return errors; + } } diff --git a/DecSm.Atom/Args/CommandLineArgsParser.cs b/src/Invex.Atom.Build/Args/CommandLineArgsParser.cs similarity index 77% rename from DecSm.Atom/Args/CommandLineArgsParser.cs rename to src/Invex.Atom.Build/Args/CommandLineArgsParser.cs index 9fe90317..6cfdae91 100644 --- a/DecSm.Atom/Args/CommandLineArgsParser.cs +++ b/src/Invex.Atom.Build/Args/CommandLineArgsParser.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Args; +namespace Invex.Atom.Build.Args; /// /// Parses raw command-line arguments into a structured object. @@ -18,7 +18,7 @@ internal sealed class CommandLineArgsParser(IBuildDefinition buildDefinition, IA /// A object representing the parsed arguments. Check the /// property to determine if parsing was successful. /// - /// + /// /// Thrown if an argument requiring a value is provided without one (e.g., --project or a defined parameter). /// /// @@ -30,24 +30,29 @@ internal sealed class CommandLineArgsParser(IBuildDefinition buildDefinition, IA /// // commandLineArgs.HasSkip will be true /// /// - public CommandLineArgs Parse(IReadOnlyList rawArgs) + public CommandLineArgs Parse(IEnumerable rawArgs) { + var rawArgsArray = rawArgs.ToArray(); + List args = []; var isValid = true; - for (var i = 0; i < rawArgs.Count; i++) + for (var i = 0; i < rawArgsArray.Length; i++) { - var rawArg = rawArgs[i]; + var rawArg = rawArgsArray[i]; if (TryParseOption(rawArg) is { } optionArg) { if (optionArg is ProjectArg) { - if (i == rawArgs.Count - 1) - throw new ArgumentException("Missing value for -[-p]roject option"); + if (i == rawArgsArray.Length - 1) + throw new CommandLineException("Missing value for -[-p]roject option. Usage: --project ") + { + ArgumentName = "project", + }; - optionArg = new ProjectArg(rawArgs[i + 1]); + optionArg = new ProjectArg(rawArgsArray[i + 1]); i++; } @@ -65,13 +70,21 @@ public CommandLineArgs Parse(IReadOnlyList rawArgs) foreach (var buildParam in buildDefinition.ParamDefinitions.Where(buildParam => string.Equals(argParam, buildParam.Value.ArgName, StringComparison.OrdinalIgnoreCase))) { - if (i == rawArgs.Count - 1) - throw new ArgumentException($"Missing value for parameter '{argParam}'"); + if (i == rawArgsArray.Length - 1) + throw new CommandLineException( + $"Missing value for parameter '{argParam}'. Usage: --{argParam} ") + { + ArgumentName = argParam, + }; - var nextArg = rawArgs[i + 1]; + var nextArg = rawArgsArray[i + 1]; if (nextArg.StartsWith("--")) - throw new ArgumentException($"Missing value for parameter '{argParam}'"); + throw new CommandLineException( + $"Missing value for parameter '{argParam}'. The next argument '{nextArg}' looks like another option. Usage: --{argParam} ") + { + ArgumentName = argParam, + }; args.Add(new ParamArg(buildParam.Value.ArgName, buildParam.Key, nextArg)); i++; @@ -144,7 +157,6 @@ public CommandLineArgs Parse(IReadOnlyList rawArgs) rawArg.ToLower() switch { "-h" or "--help" => new HelpArg(), - "-g" or "--gen" => new GenArg(), "-s" or "--skip" => new SkipArg(), "-hl" or "--headless" => new HeadlessArg(), "-v" or "--verbose" => new VerboseArg(), @@ -158,12 +170,17 @@ public CommandLineArgs Parse(IReadOnlyList rawArgs) /// /// The available target definitions. /// The raw string argument. - /// A if the argument matches a known target; otherwise, null. - private static CommandArg? TryParseCommand(IReadOnlyDictionary targetDefinitions, string rawArg) => - targetDefinitions + /// A if the argument matches a known target or alias; otherwise, null. + private static CommandArg? TryParseCommand(IReadOnlyDictionary targetDefinitions, string rawArg) + { + // Match by target name + var matchedByName = targetDefinitions .Where(buildTarget => string.Equals(rawArg, buildTarget.Key, StringComparison.OrdinalIgnoreCase)) .Select(x => x.Key) - .FirstOrDefault() is { } matchedBuildTarget - ? new CommandArg(matchedBuildTarget) + .FirstOrDefault(); + + return matchedByName is not null + ? new(matchedByName) : null; + } } diff --git a/DecSm.Atom/Artifacts/IArtifactProvider.cs b/src/Invex.Atom.Build/Artifacts/IArtifactProvider.cs similarity index 84% rename from DecSm.Atom/Artifacts/IArtifactProvider.cs rename to src/Invex.Atom.Build/Artifacts/IArtifactProvider.cs index 34e38127..4466dcb0 100644 --- a/DecSm.Atom/Artifacts/IArtifactProvider.cs +++ b/src/Invex.Atom.Build/Artifacts/IArtifactProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Artifacts; +namespace Invex.Atom.Build.Artifacts; /// /// Defines a provider for storing and retrieving build artifacts. @@ -33,13 +33,8 @@ public interface IArtifactProvider /// /// A cancellation token to observe while waiting for the task to complete. /// A representing the asynchronous upload operation. - /// - /// Artifacts are typically sourced from the directory specified by - /// . - /// This method is called by targets like . - /// Task StoreArtifacts( - IReadOnlyList artifactNames, + IEnumerable artifactNames, string? buildId = null, string? buildSlice = null, CancellationToken cancellationToken = default); @@ -55,13 +50,8 @@ Task StoreArtifacts( /// An optional identifier for a specific build variation to retrieve artifacts from. /// A cancellation token to observe while waiting for the task to complete. /// A representing the asynchronous download operation. - /// - /// Downloaded artifacts are placed in the directory specified by - /// . - /// This method is called by targets like . - /// Task RetrieveArtifacts( - IReadOnlyList artifactNames, + IEnumerable artifactNames, string? buildId = null, string? buildSlice = null, CancellationToken cancellationToken = default); @@ -75,7 +65,7 @@ Task RetrieveArtifacts( /// /// This is useful for managing storage by removing old or temporary artifacts. /// - Task Cleanup(IReadOnlyList runIdentifiers, CancellationToken cancellationToken = default); + Task Cleanup(IEnumerable runIdentifiers, CancellationToken cancellationToken = default); /// /// Retrieves a list of all stored run identifiers from the artifact storage. diff --git a/DecSm.Atom/Artifacts/IAtomArtifactsParam.cs b/src/Invex.Atom.Build/Artifacts/IAtomArtifactsParam.cs similarity index 96% rename from DecSm.Atom/Artifacts/IAtomArtifactsParam.cs rename to src/Invex.Atom.Build/Artifacts/IAtomArtifactsParam.cs index d947d505..0bbd2f13 100644 --- a/DecSm.Atom/Artifacts/IAtomArtifactsParam.cs +++ b/src/Invex.Atom.Build/Artifacts/IAtomArtifactsParam.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Artifacts; +namespace Invex.Atom.Build.Artifacts; /// /// Provides a shared parameter for specifying artifact names. @@ -9,6 +9,7 @@ /// The parameter is typically supplied via the --atom-artifacts command-line argument or an environment /// variable. /// +[PublicAPI] public interface IAtomArtifactsParam : IBuildAccessor { /// diff --git a/DecSm.Atom/Artifacts/IRetrieveArtifact.cs b/src/Invex.Atom.Build/Artifacts/IRetrieveArtifact.cs similarity index 97% rename from DecSm.Atom/Artifacts/IRetrieveArtifact.cs rename to src/Invex.Atom.Build/Artifacts/IRetrieveArtifact.cs index e0fb29b2..5331946e 100644 --- a/DecSm.Atom/Artifacts/IRetrieveArtifact.cs +++ b/src/Invex.Atom.Build/Artifacts/IRetrieveArtifact.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Artifacts; +namespace Invex.Atom.Build.Artifacts; /// /// Defines a target for retrieving build artifacts using an . @@ -11,6 +11,7 @@ /// to identify the correct artifacts to download. /// This target is hidden by default, primarily for internal use or custom artifact workflows. /// +[PublicAPI] public interface IRetrieveArtifact : IAtomArtifactsParam, ISetupBuildInfo { /// diff --git a/DecSm.Atom/Artifacts/IStoreArtifact.cs b/src/Invex.Atom.Build/Artifacts/IStoreArtifact.cs similarity index 94% rename from DecSm.Atom/Artifacts/IStoreArtifact.cs rename to src/Invex.Atom.Build/Artifacts/IStoreArtifact.cs index 5ebe70bf..8d6c50a0 100644 --- a/DecSm.Atom/Artifacts/IStoreArtifact.cs +++ b/src/Invex.Atom.Build/Artifacts/IStoreArtifact.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Artifacts; +namespace Invex.Atom.Build.Artifacts; /// /// Defines a target for storing build artifacts using an . @@ -7,11 +7,12 @@ /// This interface, when implemented by a build definition, provides the target. /// It uses the configured to upload artifacts. /// Artifacts to be stored are specified via the parameter -/// and are expected to be located in the . +/// and are expected to be located in the AtomPublishDirectory. /// Build information from (e.g., BuildName, BuildId) is used /// to categorize or tag the artifacts. /// This target is hidden by default, primarily for internal use or custom artifact workflows. /// +[PublicAPI] public interface IStoreArtifact : IAtomArtifactsParam, ISetupBuildInfo { /// diff --git a/src/Invex.Atom.Build/AtomProjectData.cs b/src/Invex.Atom.Build/AtomProjectData.cs new file mode 100644 index 00000000..b697a3d5 --- /dev/null +++ b/src/Invex.Atom.Build/AtomProjectData.cs @@ -0,0 +1,15 @@ +namespace Invex.Atom.Build; + +[PublicAPI] +public sealed record AtomProjectData +{ + /// + /// The name of the project, typically derived from the entry assembly. + /// + public required string ProjectName { get; init; } + + /// + /// Whether the application is file-based (e.g., *.cs) or project-based (e.g., *.csproj). + /// + public required bool IsFileBasedApp { get; init; } +} diff --git a/DecSm.Atom/AtomService.cs b/src/Invex.Atom.Build/AtomService.cs similarity index 62% rename from DecSm.Atom/AtomService.cs rename to src/Invex.Atom.Build/AtomService.cs index b26ff8fb..25495e7b 100644 --- a/DecSm.Atom/AtomService.cs +++ b/src/Invex.Atom.Build/AtomService.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom; +namespace Invex.Atom.Build; /// /// Represents the core background service for the Atom build framework. @@ -15,13 +15,6 @@ /// Displaying help information via . /// /// -/// -/// Generating CI/CD workflow files (e.g., GitHub Actions) using if -/// specified or -/// if not running in headless mode. -/// -/// -/// /// Executing the defined build targets using . /// /// @@ -36,7 +29,7 @@ /// This service is registered and managed by the .NET Generic Host ( /// ) /// and is typically configured via the AddAtom extension method in the -/// DecSm.Atom.Hosting.HostExtensions class. +/// Invex.Atom.Hosting.HostExtensions class. /// /// /// While AtomService is internal, its operation is primarily influenced by command-line arguments passed to @@ -57,12 +50,6 @@ /// This will cause AtomService to invoke . /// Generating workflows: /// -/// atom --gen -/// -/// This instructs AtomService to regenerate workflow files via -/// . -/// Executing a specific build target: -/// /// atom Build /// /// This will lead AtomService to use to run the "Build" target. @@ -70,17 +57,18 @@ /// /// atom Build --headless /// -/// In this mode, workflow generation is typically skipped. If workflows are found to be "dirty" (outdated), +/// In this mode, workflow generation is typically skipped. If workflows are found to be outdated, /// AtomService will raise an error, prompting regeneration. /// -/// +/// /// internal sealed class AtomService( CommandLineArgs args, BuildExecutor executor, IHelpService helpService, - WorkflowGenerator workflowGenerator, IHostApplicationLifetime lifetime, + ReportService reportService, + IEnumerable lifecycleHooks, ILogger logger ) : BackgroundService { @@ -89,34 +77,63 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) try { if (!args.IsValid) - throw new ArgumentException("Invalid command-line arguments"); - - if (args is { HasHelp: false, HasHeadless: false, HasGen: false, Commands.Count: 0, Params.Count: 0 }) { - await workflowGenerator.GenerateWorkflows(stoppingToken); - helpService.ShowHelp(); + var errors = args.GetValidationErrors(); - return; + var errorMessage = string.Join(Environment.NewLine, + "Invalid command-line arguments:", + string.Join(Environment.NewLine, errors.Select(e => $" - {e}")), + "", + "Run with --help for usage information."); + + throw new CommandLineException(errorMessage); } - if (args.HasHelp) - { + if (args.HasHelp || !args.Commands.Any()) helpService.ShowHelp(); + if (args is { HasHelp: true } or { HasHelp: false, HasHeadless: false, Commands.Count: 0, Params.Count: 0 }) return; - } - if (args.HasGen || !args.HasHeadless) - await workflowGenerator.GenerateWorkflows(stoppingToken); - else if (await workflowGenerator.WorkflowsDirty(stoppingToken)) - throw new InvalidOperationException( - "One or more workflows are dirty. Run 'atom -g' to regenerate them"); + foreach (var hook in lifecycleHooks) + await hook.BeforeExecute(stoppingToken); await executor.Execute(stoppingToken); + + foreach (var hook in lifecycleHooks) + await hook.AfterExecute(stoppingToken); + } + catch (CommandLineException ex) + { + logger.LogError("Invalid command-line arguments. {Message}", ex.Message); + + if (ex.ArgumentName is not null) + logger.LogError("Problematic argument: {ArgumentName}", ex.ArgumentName); + + Environment.ExitCode = 1; + } + catch (BuildConfigurationException ex) + { + logger.LogCritical("Build configuration error: {Message}", ex.Message); + + if (ex.ReportData is not null) + reportService.AddReportData(ex.ReportData); + + Environment.ExitCode = 1; + } + catch (StepFailedException) + { + // Already handled by BuildExecutor, just set exit code + Environment.ExitCode = 1; + } + catch (AtomException ex) + { + logger.LogCritical("{Message}", ex.Message); + Environment.ExitCode = 1; } catch (Exception ex) { - logger.LogError(ex, "Stopped"); + logger.LogCritical(ex, "An unexpected error occurred. Please report this issue."); Environment.ExitCode = 1; } finally diff --git a/DecSm.Atom/Build/BuildExecutor.cs b/src/Invex.Atom.Build/BuildExecutor.cs similarity index 80% rename from DecSm.Atom/Build/BuildExecutor.cs rename to src/Invex.Atom.Build/BuildExecutor.cs index f1947d2d..a4e2b3f3 100644 --- a/DecSm.Atom/Build/BuildExecutor.cs +++ b/src/Invex.Atom.Build/BuildExecutor.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build; +namespace Invex.Atom.Build; /// /// Responsible for executing build targets based on command-line arguments and the build model. @@ -15,7 +15,7 @@ internal sealed class BuildExecutor( CommandLineArgs args, BuildModel buildModel, IParamService paramService, - IWorkflowVariableService variableService, + IVariableService variableService, IEnumerable outcomeReporters, IAnsiConsole console, ReportService reportService, @@ -71,6 +71,8 @@ public async Task Execute(CancellationToken cancellationToken) /// The target to validate. private void ValidateTargetParameters(TargetModel target) { + var missingParams = new List(); + foreach (var requiredParam in target.Params.Where(x => x.Required)) { var defaultValue = requiredParam.Param.DefaultValue is { Length: > 0 } @@ -92,14 +94,29 @@ private void ValidateTargetParameters(TargetModel target) if (value is { Length: > 0 }) continue; - logger.LogError("Missing required parameter '{ParamName}' for target {TargetDefinitionName}", - requiredParam.Param.ArgName, - target.Name); - - buildModel.TargetStates[target].Status = TargetRunState.Failed; + missingParams.Add(requiredParam); + } + if (missingParams.Count == 0) return; - } + + foreach (var requiredParam in missingParams) + logger.LogError(""" + Missing required parameter '{ParamName}' for target {TargetName}. + You can provide it via: + Command line: --{ArgName} , + Environment variable: {EnvVarName}, + appsettings.json file: Params:{ConfigParamName}, + Interactive mode: -i or --interactive + """, + requiredParam.Param.ArgName, + target.Name, + requiredParam.Param.ArgName, + requiredParam.Param.EnvVarName, + requiredParam.Param.Name); + + buildModel.GetTargetState(target) + .Status = TargetRunState.Failed; } /// @@ -109,17 +126,19 @@ private void ValidateTargetParameters(TargetModel target) /// A cancellation token to observe. private async Task ExecuteTarget(TargetModel target, CancellationToken cancellationToken) { - if (buildModel.TargetStates[target].Status is TargetRunState.NotRun + var targetState = buildModel.GetTargetState(target); + + if (targetState.Status is TargetRunState.NotRun or TargetRunState.Skipped or TargetRunState.Succeeded or TargetRunState.Failed) return; - if (buildModel.TargetStates[target].Status is not TargetRunState.PendingRun) + if (targetState.Status is not TargetRunState.PendingRun) { logger.LogWarning("Skipping target {TargetDefinitionName} due to unexpected state {TargetState}", target.Name, - buildModel.TargetStates[target].Status); + targetState.Status); return; } @@ -127,9 +146,11 @@ or TargetRunState.Succeeded foreach (var dependency in target.Dependencies) await ExecuteTarget(dependency, cancellationToken); - if (target.Dependencies.Any(depTarget => buildModel.TargetStates[depTarget].Status is TargetRunState.Failed)) + if (target.Dependencies.Any(depTarget => buildModel.GetTargetState(depTarget) + .Status is TargetRunState.Failed)) { - buildModel.TargetStates[target].Status = TargetRunState.NotRun; + targetState.Status = TargetRunState.NotRun; + logger.LogWarning("Skipping target {TargetDefinitionName} due to failed dependencies", target.Name); return; @@ -147,12 +168,12 @@ or TargetRunState.Succeeded variable.TargetName, target.Name); - buildModel.TargetStates[target].Status = TargetRunState.Failed; + targetState.Status = TargetRunState.Failed; return; } - buildModel.TargetStates[target].Status = TargetRunState.Running; + targetState.Status = TargetRunState.Running; var startTime = Stopwatch.GetTimestamp(); @@ -189,7 +210,7 @@ or TargetRunState.Succeeded await task(cancellationToken); } - buildModel.TargetStates[target].Status = TargetRunState.Succeeded; + targetState.Status = TargetRunState.Succeeded; } catch (StepFailedException failedCheckException) { @@ -197,7 +218,7 @@ or TargetRunState.Succeeded "A check failed for target {TargetDefinitionName}", target.Name); - buildModel.TargetStates[target].Status = TargetRunState.Failed; + targetState.Status = TargetRunState.Failed; reportService.AddReportData(new TextReportData(failedCheckException.Message) { @@ -210,10 +231,11 @@ or TargetRunState.Succeeded catch (Exception ex) { logger.LogError(ex, "An error occurred while executing target {TargetDefinitionName}", target.Name); - buildModel.TargetStates[target].Status = TargetRunState.Failed; + + targetState.Status = TargetRunState.Failed; } - buildModel.TargetStates[target].RunDuration = + targetState.RunDuration = TimeSpan.FromSeconds((Stopwatch.GetTimestamp() - startTime) / (double)Stopwatch.Frequency); if (args.HasHeadless) diff --git a/DecSm.Atom/BuildInfo/DefaultBuildIdProvider.cs b/src/Invex.Atom.Build/BuildInfo/DefaultBuildIdProvider.cs similarity index 96% rename from DecSm.Atom/BuildInfo/DefaultBuildIdProvider.cs rename to src/Invex.Atom.Build/BuildInfo/DefaultBuildIdProvider.cs index 46c4d4cb..60211e2c 100644 --- a/DecSm.Atom/BuildInfo/DefaultBuildIdProvider.cs +++ b/src/Invex.Atom.Build/BuildInfo/DefaultBuildIdProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.BuildInfo; +namespace Invex.Atom.Build.BuildInfo; /// /// The default provider for generating a build ID, used when no custom implementation is registered. diff --git a/DecSm.Atom/BuildInfo/DefaultBuildTimestampProvider.cs b/src/Invex.Atom.Build/BuildInfo/DefaultBuildTimestampProvider.cs similarity index 97% rename from DecSm.Atom/BuildInfo/DefaultBuildTimestampProvider.cs rename to src/Invex.Atom.Build/BuildInfo/DefaultBuildTimestampProvider.cs index f276c3d0..22fdb5a8 100644 --- a/DecSm.Atom/BuildInfo/DefaultBuildTimestampProvider.cs +++ b/src/Invex.Atom.Build/BuildInfo/DefaultBuildTimestampProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.BuildInfo; +namespace Invex.Atom.Build.BuildInfo; /// /// The default provider for determining the build timestamp, used when no custom implementation is registered. diff --git a/DecSm.Atom/BuildInfo/DefaultBuildVersionProvider.cs b/src/Invex.Atom.Build/BuildInfo/DefaultBuildVersionProvider.cs similarity index 95% rename from DecSm.Atom/BuildInfo/DefaultBuildVersionProvider.cs rename to src/Invex.Atom.Build/BuildInfo/DefaultBuildVersionProvider.cs index 97ffe42c..f90eb852 100644 --- a/DecSm.Atom/BuildInfo/DefaultBuildVersionProvider.cs +++ b/src/Invex.Atom.Build/BuildInfo/DefaultBuildVersionProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.BuildInfo; +namespace Invex.Atom.Build.BuildInfo; /// /// The default provider for determining the build version, used when no custom implementation is registered. @@ -8,7 +8,7 @@ /// It searches for common version-related MSBuild properties and falls back to "1.0.0" if no version is found. /// /// The file system service for accessing project files. -internal sealed partial class DefaultBuildVersionProvider(IAtomFileSystem fileSystem) : IBuildVersionProvider +internal sealed partial class DefaultBuildVersionProvider(IRootedFileSystem fileSystem) : IBuildVersionProvider { /// /// Gets the build version by parsing a Directory.Build.props file. diff --git a/DecSm.Atom/BuildInfo/IBuildIdProvider.cs b/src/Invex.Atom.Build/BuildInfo/IBuildIdProvider.cs similarity index 95% rename from DecSm.Atom/BuildInfo/IBuildIdProvider.cs rename to src/Invex.Atom.Build/BuildInfo/IBuildIdProvider.cs index 356cd1fe..4fa103d3 100644 --- a/DecSm.Atom/BuildInfo/IBuildIdProvider.cs +++ b/src/Invex.Atom.Build/BuildInfo/IBuildIdProvider.cs @@ -1,8 +1,9 @@ -namespace DecSm.Atom.BuildInfo; +namespace Invex.Atom.Build.BuildInfo; /// /// Defines a provider for generating a unique build identifier. /// +[PublicAPI] public interface IBuildIdProvider { /// diff --git a/DecSm.Atom/BuildInfo/IBuildInfo.cs b/src/Invex.Atom.Build/BuildInfo/IBuildInfo.cs similarity index 87% rename from DecSm.Atom/BuildInfo/IBuildInfo.cs rename to src/Invex.Atom.Build/BuildInfo/IBuildInfo.cs index 3a4a6b94..5d927761 100644 --- a/DecSm.Atom/BuildInfo/IBuildInfo.cs +++ b/src/Invex.Atom.Build/BuildInfo/IBuildInfo.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.BuildInfo; +namespace Invex.Atom.Build.BuildInfo; /// /// Provides centralized access to essential build metadata, such as version, name, and identifiers. @@ -9,6 +9,7 @@ /// . This ensures that a stable value is captured at the start of the workflow and /// reused across all targets, providing consistency. /// +[PublicAPI] public interface IBuildInfo : IBuildAccessor { /// @@ -81,23 +82,26 @@ private string DefaultBuildName { get { - var solutionFile = FileSystem + var solutionFile = RootedFileSystem .Directory - .GetFiles(FileSystem.AtomRootDirectory, "*.slnx", SearchOption.TopDirectoryOnly) + .GetFiles(RootedFileSystem.AtomRootDirectory, + "*.slnx", + SearchOption.TopDirectoryOnly) .FirstOrDefault() ?? - FileSystem + RootedFileSystem .Directory - .GetFiles(FileSystem.AtomRootDirectory, "*.sln", SearchOption.TopDirectoryOnly) + .GetFiles(RootedFileSystem.AtomRootDirectory, "*.sln", SearchOption.TopDirectoryOnly) .FirstOrDefault(); Logger.LogDebug("Determined solution file: {SolutionFile}", solutionFile); return solutionFile is not null - ? new RootedPath(FileSystem, solutionFile).FileNameWithoutExtension - : FileSystem + ? new RootedPath(RootedFileSystem, solutionFile).FileNameWithoutExtension + : RootedFileSystem .AtomRootDirectory .DirectoryName - ?.Split(FileSystem.Path.DirectorySeparatorChar, FileSystem.Path.AltDirectorySeparatorChar)[^1] ?? + ?.Split(RootedFileSystem.Path.DirectorySeparatorChar, + RootedFileSystem.Path.AltDirectorySeparatorChar)[^1] ?? "Unknown"; } } diff --git a/DecSm.Atom/BuildInfo/IBuildTimestampProvider.cs b/src/Invex.Atom.Build/BuildInfo/IBuildTimestampProvider.cs similarity index 91% rename from DecSm.Atom/BuildInfo/IBuildTimestampProvider.cs rename to src/Invex.Atom.Build/BuildInfo/IBuildTimestampProvider.cs index 6141b4d6..4ff848a4 100644 --- a/DecSm.Atom/BuildInfo/IBuildTimestampProvider.cs +++ b/src/Invex.Atom.Build/BuildInfo/IBuildTimestampProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.BuildInfo; +namespace Invex.Atom.Build.BuildInfo; /// /// Defines a provider for determining the build timestamp. diff --git a/DecSm.Atom/BuildInfo/IBuildVersionProvider.cs b/src/Invex.Atom.Build/BuildInfo/IBuildVersionProvider.cs similarity index 91% rename from DecSm.Atom/BuildInfo/IBuildVersionProvider.cs rename to src/Invex.Atom.Build/BuildInfo/IBuildVersionProvider.cs index 99040e2c..0bb6dc2f 100644 --- a/DecSm.Atom/BuildInfo/IBuildVersionProvider.cs +++ b/src/Invex.Atom.Build/BuildInfo/IBuildVersionProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.BuildInfo; +namespace Invex.Atom.Build.BuildInfo; /// /// Defines a provider for determining the semantic version of the build. diff --git a/src/Invex.Atom.Build/BuildOptions/BuildOptionExtensions.cs b/src/Invex.Atom.Build/BuildOptions/BuildOptionExtensions.cs new file mode 100644 index 00000000..5a009fce --- /dev/null +++ b/src/Invex.Atom.Build/BuildOptions/BuildOptionExtensions.cs @@ -0,0 +1,62 @@ +namespace Invex.Atom.Build.BuildOptions; + +[PublicAPI] +public static class BuildOptionExtensions +{ + extension(T) + where T : IBuildOption + { + [PublicAPI] + public static IReadOnlyList GetOptions(IEnumerable options) => + options + .OfType() + .ToList(); + + [PublicAPI] + public static IReadOnlyList GetOptions(IBuildDefinition definition) => + definition + .Options + .OfType() + .ToList(); + + [PublicAPI] + public static IReadOnlyList GetOptionsGrouped( + IEnumerable options, + Func groupBy) => + options + .OfType() + .GroupBy(groupBy) + .Select(x => x.Last()) + .ToList(); + + [PublicAPI] + public static T? Get(IEnumerable options) => + options + .OfType() + .LastOrDefault(); + + [PublicAPI] + public static T? Get(IBuildDefinition definition) => + definition + .Options + .OfType() + .LastOrDefault(); + } + + extension(T) + where T : ToggleBuildOption + { + [PublicAPI] + public static bool IsEnabled(IEnumerable options) => + options + .OfType() + .LastOrDefault() is { Enabled: true }; + + [PublicAPI] + public static bool IsEnabled(IBuildDefinition definition) => + definition + .Options + .OfType() + .LastOrDefault() is { Enabled: true }; + } +} diff --git a/src/Invex.Atom.Build/BuildOptions/BuildOptionService.cs b/src/Invex.Atom.Build/BuildOptions/BuildOptionService.cs new file mode 100644 index 00000000..44148c18 --- /dev/null +++ b/src/Invex.Atom.Build/BuildOptions/BuildOptionService.cs @@ -0,0 +1,28 @@ +namespace Invex.Atom.Build.BuildOptions; + +/// +/// Provides access to the fully resolved set of build options, merging +/// with contributions from all registered +/// instances. +/// +[PublicAPI] +public interface IBuildOptionService +{ + /// + /// Gets the merged set of build options from + /// and all registered instances. + /// + IReadOnlyList Options { get; } +} + +internal sealed class BuildOptionService(IBuildDefinition buildDefinition, IEnumerable providers) + : IBuildOptionService +{ + private readonly IReadOnlyList _providers = providers.ToList(); + + public IReadOnlyList Options => + field ??= buildDefinition + .Options + .Concat(_providers.SelectMany(p => p.GetBuildOptions(buildDefinition.Options))) + .ToList(); +} diff --git a/src/Invex.Atom.Build/BuildOptions/BuildOptions.cs b/src/Invex.Atom.Build/BuildOptions/BuildOptions.cs new file mode 100644 index 00000000..8aa534af --- /dev/null +++ b/src/Invex.Atom.Build/BuildOptions/BuildOptions.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Build.BuildOptions; + +[PublicAPI] +public static class BuildOptions; diff --git a/src/Invex.Atom.Build/BuildOptions/IBuildOption.cs b/src/Invex.Atom.Build/BuildOptions/IBuildOption.cs new file mode 100644 index 00000000..21299bb0 --- /dev/null +++ b/src/Invex.Atom.Build/BuildOptions/IBuildOption.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Build.BuildOptions; + +[PublicAPI] +public interface IBuildOption; diff --git a/src/Invex.Atom.Build/BuildOptions/IBuildOptionProvider.cs b/src/Invex.Atom.Build/BuildOptions/IBuildOptionProvider.cs new file mode 100644 index 00000000..06406355 --- /dev/null +++ b/src/Invex.Atom.Build/BuildOptions/IBuildOptionProvider.cs @@ -0,0 +1,22 @@ +namespace Invex.Atom.Build.BuildOptions; + +/// +/// Defines a contract for contributing build options to the resolved option set. +/// +/// +/// Implementations receive the base options from and may +/// return additional options to be merged in. Uses two-phase resolution: providers only receive the base +/// options, not other providers' contributions, preventing inter-provider dependencies. +/// Register implementations with DI as to have them automatically +/// included. +/// +[PublicAPI] +public interface IBuildOptionProvider +{ + /// + /// Gets additional build options based on the current base options from . + /// + /// The base options from . + /// Additional build options to merge into the resolved set. + IReadOnlyList GetBuildOptions(IReadOnlyList baseOptions); +} diff --git a/src/Invex.Atom.Build/BuildOptions/ToggleBuildOption.cs b/src/Invex.Atom.Build/BuildOptions/ToggleBuildOption.cs new file mode 100644 index 00000000..6fdb2655 --- /dev/null +++ b/src/Invex.Atom.Build/BuildOptions/ToggleBuildOption.cs @@ -0,0 +1,7 @@ +namespace Invex.Atom.Build.BuildOptions; + +[PublicAPI] +public abstract record ToggleBuildOption : IBuildOption +{ + public bool Enabled { get; init; } = true; +} diff --git a/DecSm.Atom/Build/BuildResolver.cs b/src/Invex.Atom.Build/BuildResolver.cs similarity index 81% rename from DecSm.Atom/Build/BuildResolver.cs rename to src/Invex.Atom.Build/BuildResolver.cs index 5428ec93..65520744 100644 --- a/DecSm.Atom/Build/BuildResolver.cs +++ b/src/Invex.Atom.Build/BuildResolver.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build; +namespace Invex.Atom.Build; /// /// Resolves the complete by processing target definitions, dependencies, and parameters. @@ -18,7 +18,9 @@ ILogger logger /// Resolves and constructs the for the current build. /// /// A fully resolved instance. - /// Thrown if duplicate or circular target dependencies are detected. + /// Thrown when duplicate target names are detected. + /// Thrown when a target depends on a non-existent target. + /// Thrown when circular dependencies are detected between targets. public BuildModel Resolve() { var startTime = Stopwatch.GetTimestamp(); @@ -71,8 +73,16 @@ public BuildModel Resolve() .ToArray(); if (duplicateTargetNames.Length > 0) - throw new( - $"One or more targets are defined multiple times, which is not allowed: {string.Join(", ", $"'{duplicateTargetNames}'")}."); + throw new BuildConfigurationException( + $"One or more targets are defined multiple times, which is not allowed: {string.Join(", ", duplicateTargetNames.Select(n => $"'{n}'"))}.") + { + ReportData = new ListReportData(duplicateTargetNames + .Select(n => $"Target '{n}' is defined multiple times") + .ToList()) + { + Title = "Duplicate Targets", + }, + }; // Allows mutation of target model dependencies within this scope var targetModelDependencyMap = new Dictionary>(); @@ -89,7 +99,7 @@ public BuildModel Resolve() var usedParams = new List(); foreach (var param in x.Params) - AddParamAndChildren(param.Param, param.Required, usedParams, paramModels); + AddParamAndChildren(param.Param, param.Required, usedParams, paramModels, []); return new TargetModel(x.Name, x.Description, x.Hidden) { @@ -121,7 +131,7 @@ public BuildModel Resolve() var dependencyTargetDefinition = targetDefinitions.FirstOrDefault(x => x.Name == dependencyName); if (dependencyTargetDefinition is null) - throw new( + throw new BuildConfigurationException( $"Target '{targetModel.Name}' depends on target '{dependencyName}' which does not exist."); targetModelDependencyMap[targetModel.Name] @@ -132,6 +142,7 @@ public BuildModel Resolve() // Sort targets and find circular dependencies var depthFirstTargets = new List(); var targetMarks = targetModels.ToDictionary(x => x, _ => (Temporary: false, Permenant: false)); + var visitStack = new Stack(); for (var target = targetModels.FirstOrDefault(x => !targetMarks[x].Permenant); target is not null; @@ -186,6 +197,7 @@ public BuildModel Resolve() .Where(state => state.Status is not TargetRunState.PendingRun)) state.Status = TargetRunState.Skipped; + // ReSharper disable once InvertIf if (logger.IsEnabled(LogLevel.Debug)) { var endTime = Stopwatch.GetTimestamp(); @@ -209,14 +221,38 @@ void Visit(TargetModel target) return; if (marks.Temporary) - throw new( - $"Circular dependency detected: {string.Join(" -> ", depthFirstTargets.Select(x => x.Name))}."); + { + var cycle = new List + { + target.Name, + }; + + foreach (var name in visitStack) + { + cycle.Add(name); + + if (name == target.Name) + break; + } + + cycle.Reverse(); + + throw new BuildConfigurationException($"Circular dependency detected: {string.Join(" -> ", cycle)}") + { + ReportData = new TextReportData($"Dependency cycle:\n {string.Join("\n -> ", cycle)}") + { + Title = "Circular Dependency Detected", + }, + }; + } targetMarks[target] = (true, marks.Permenant); + visitStack.Push(target.Name); foreach (var dependency in target.Dependencies) Visit(dependency); + visitStack.Pop(); targetMarks[target] = (false, true); depthFirstTargets.Insert(0, target); } @@ -229,16 +265,21 @@ void Visit(TargetModel target) /// A value indicating whether the parameter is required. /// The list of used parameters to add to. /// A dictionary of all available parameter models. + /// A set of already-visited parameter names to detect and prevent circular chains. private static void AddParamAndChildren( string param, bool required, List usedParams, - Dictionary paramModels) + Dictionary paramModels, + HashSet visited) { + if (!visited.Add(param)) + return; + var model = paramModels[param]; usedParams.Add(new(model, required)); foreach (var chainedParam in model.ChainedParams) - AddParamAndChildren(chainedParam, required, usedParams, paramModels); + AddParamAndChildren(chainedParam, required, usedParams, paramModels, visited); } } diff --git a/src/Invex.Atom.Build/Definition/BuildDefinition.cs b/src/Invex.Atom.Build/Definition/BuildDefinition.cs new file mode 100644 index 00000000..55413a97 --- /dev/null +++ b/src/Invex.Atom.Build/Definition/BuildDefinition.cs @@ -0,0 +1,43 @@ +namespace Invex.Atom.Build.Definition; + +/// +/// The standard abstract base class for creating build definitions, providing default implementations for +/// . +/// +/// +/// The and properties are populated by source +/// generators based on the interfaces and attributes used in the derived class. A derived class must be +/// decorated with . +/// +/// +/// A typical build definition: +/// +/// [BuildDefinition] +/// internal partial class MyBuild : BuildDefinition, IMyTargets +/// { +/// // ... +/// } +/// +/// +/// The service provider for dependency injection. +[PublicAPI] +public abstract class BuildDefinition(IServiceProvider services) : IBuildDefinition +{ + /// + /// Provides access to the service provider associated with the build definition. + /// This property allows resolving dependencies and accessing services registered in the application. + /// + public IServiceProvider Services => services; + + /// + public abstract IReadOnlyDictionary TargetDefinitions { get; } + + /// + public abstract IReadOnlyDictionary ParamDefinitions { get; } + + /// + public abstract object? AccessParam(string paramName); + + /// + public virtual void ConfigureDefinitionHost(IHostApplicationBuilder builder) { } +} diff --git a/DecSm.Atom/Build/Definition/BuildDefinitionAttribute.cs b/src/Invex.Atom.Build/Definition/BuildDefinitionAttribute.cs similarity index 81% rename from DecSm.Atom/Build/Definition/BuildDefinitionAttribute.cs rename to src/Invex.Atom.Build/Definition/BuildDefinitionAttribute.cs index 0636daed..bc4dfefd 100644 --- a/DecSm.Atom/Build/Definition/BuildDefinitionAttribute.cs +++ b/src/Invex.Atom.Build/Definition/BuildDefinitionAttribute.cs @@ -1,10 +1,10 @@ -namespace DecSm.Atom.Build.Definition; +namespace Invex.Atom.Build.Definition; /// /// Marks a class as a build definition, triggering source generation to implement . /// /// -/// This attribute should be applied to a class that inherits from or +/// This attribute should be applied to a class that inherits from or /// . /// The source generator uses this attribute to identify the main build class and generate the necessary code to /// discover and register targets and parameters. @@ -19,5 +19,5 @@ /// /// [PublicAPI] -[AttributeUsage(AttributeTargets.Class)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)] public sealed class BuildDefinitionAttribute : Attribute; diff --git a/src/Invex.Atom.Build/Definition/BuildDefinitionInterfaceAttribute.cs b/src/Invex.Atom.Build/Definition/BuildDefinitionInterfaceAttribute.cs new file mode 100644 index 00000000..98f776b4 --- /dev/null +++ b/src/Invex.Atom.Build/Definition/BuildDefinitionInterfaceAttribute.cs @@ -0,0 +1,5 @@ +namespace Invex.Atom.Build.Definition; + +[PublicAPI] +[AttributeUsage(AttributeTargets.Interface)] +public sealed class BuildDefinitionInterfaceAttribute : Attribute; diff --git a/DecSm.Atom/Build/Definition/DefinedParam.cs b/src/Invex.Atom.Build/Definition/DefinedParam.cs similarity index 86% rename from DecSm.Atom/Build/Definition/DefinedParam.cs rename to src/Invex.Atom.Build/Definition/DefinedParam.cs index ea97caec..adc6ba66 100644 --- a/DecSm.Atom/Build/Definition/DefinedParam.cs +++ b/src/Invex.Atom.Build/Definition/DefinedParam.cs @@ -1,8 +1,9 @@ -namespace DecSm.Atom.Build.Definition; +namespace Invex.Atom.Build.Definition; /// /// Represents a parameter that is used by a target definition, specifying whether it is required. /// /// The name of the parameter. /// A value indicating whether this parameter is required. +[PublicAPI] public sealed record DefinedParam(string Param, bool Required); diff --git a/DecSm.Atom/Build/Definition/GenerateInterfaceMembersAttribute.cs b/src/Invex.Atom.Build/Definition/GenerateInterfaceMembersAttribute.cs similarity index 95% rename from DecSm.Atom/Build/Definition/GenerateInterfaceMembersAttribute.cs rename to src/Invex.Atom.Build/Definition/GenerateInterfaceMembersAttribute.cs index 26f559df..8c69a467 100644 --- a/DecSm.Atom/Build/Definition/GenerateInterfaceMembersAttribute.cs +++ b/src/Invex.Atom.Build/Definition/GenerateInterfaceMembersAttribute.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build.Definition; +namespace Invex.Atom.Build.Definition; /// /// Triggers source generation to implement members from interfaces applied to a class. diff --git a/DecSm.Atom/Build/Definition/GenerateSolutionModelAttribute.cs b/src/Invex.Atom.Build/Definition/GenerateSolutionModelAttribute.cs similarity index 88% rename from DecSm.Atom/Build/Definition/GenerateSolutionModelAttribute.cs rename to src/Invex.Atom.Build/Definition/GenerateSolutionModelAttribute.cs index 302dc765..f812e976 100644 --- a/DecSm.Atom/Build/Definition/GenerateSolutionModelAttribute.cs +++ b/src/Invex.Atom.Build/Definition/GenerateSolutionModelAttribute.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build.Definition; +namespace Invex.Atom.Build.Definition; /// /// Triggers the source generation of a solution model based on a .sln or .slnx file. @@ -19,5 +19,5 @@ namespace DecSm.Atom.Build.Definition; /// /// [PublicAPI] -[AttributeUsage(AttributeTargets.Class)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)] public sealed class GenerateSolutionModelAttribute : Attribute; diff --git a/DecSm.Atom/Build/Definition/IBuildDefinition.cs b/src/Invex.Atom.Build/Definition/IBuildDefinition.cs similarity index 68% rename from DecSm.Atom/Build/Definition/IBuildDefinition.cs rename to src/Invex.Atom.Build/Definition/IBuildDefinition.cs index 49be17ae..f1b8a75e 100644 --- a/DecSm.Atom/Build/Definition/IBuildDefinition.cs +++ b/src/Invex.Atom.Build/Definition/IBuildDefinition.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build.Definition; +namespace Invex.Atom.Build.Definition; /// /// Defines the core structure and components of an Atom build process. @@ -6,19 +6,18 @@ /// /// This interface outlines the fundamental elements of a build, including its targets, parameters, /// workflow configurations, and global options. Implementations, typically derived from -/// or , serve as the central point +/// or , serve as the central point /// for Atom to understand and execute a build. /// [PublicAPI] -public interface IBuildDefinition +[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] +public interface IBuildDefinition : IBuildAccessor { /// - /// Gets the collection of workflow definitions for the build. + /// Gets the build options applied globally to this build definition. + /// Override to supply a custom list of instances. /// - /// - /// Workflows define how targets are orchestrated, potentially across different CI/CD platforms. - /// - IReadOnlyList Workflows { get; } + IReadOnlyList Options => []; /// /// Gets the collection of target definitions for the build. @@ -39,18 +38,17 @@ public interface IBuildDefinition /// IReadOnlyDictionary ParamDefinitions { get; } - /// - /// Gets the collection of global workflow options that apply to all workflows. - /// - /// - /// These options can be overridden at the individual workflow level. - /// - IReadOnlyList GlobalWorkflowOptions { get; } - /// /// Retrieves the value of a build parameter by its name. /// /// The name of the parameter to access. /// The value of the specified parameter, or null if not defined or has no value. object? AccessParam(string paramName); + + /// + /// Allows the build definition to configure the host application builder before the host is started. + /// Override to register additional services or configuration. + /// + /// The host application builder to configure. + void ConfigureDefinitionHost(IHostApplicationBuilder builder); } diff --git a/DecSm.Atom/Build/Definition/Target.cs b/src/Invex.Atom.Build/Definition/Target.cs similarity index 89% rename from DecSm.Atom/Build/Definition/Target.cs rename to src/Invex.Atom.Build/Definition/Target.cs index 05abd472..3782cdd9 100644 --- a/DecSm.Atom/Build/Definition/Target.cs +++ b/src/Invex.Atom.Build/Definition/Target.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build.Definition; +namespace Invex.Atom.Build.Definition; /// /// Represents a delegate that applies configuration to a . diff --git a/DecSm.Atom/Build/Definition/TargetDefinition.cs b/src/Invex.Atom.Build/Definition/TargetDefinition.cs similarity index 86% rename from DecSm.Atom/Build/Definition/TargetDefinition.cs rename to src/Invex.Atom.Build/Definition/TargetDefinition.cs index 18954874..d9903715 100644 --- a/DecSm.Atom/Build/Definition/TargetDefinition.cs +++ b/src/Invex.Atom.Build/Definition/TargetDefinition.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build.Definition; +namespace Invex.Atom.Build.Definition; /// /// Defines a target that can be modeled and executed, including its tasks, dependencies, and parameters. @@ -91,12 +91,37 @@ internal TargetDefinition ApplyExtensions(IBuildDefinition buildDefinition) if (extension.RunExtensionAfter) { Tasks.AddRange(targetToExtend.Tasks); - Dependencies.AddRange(targetToExtend.Dependencies); - Params.AddRange(targetToExtend.Params); - ConsumedArtifacts.AddRange(targetToExtend.ConsumedArtifacts); - ProducedArtifacts.AddRange(targetToExtend.ProducedArtifacts); - ConsumedVariables.AddRange(targetToExtend.ConsumedVariables); - ProducedVariables.AddRange(targetToExtend.ProducedVariables); + + Dependencies = Dependencies + .Concat(targetToExtend.Dependencies) + .Distinct() + .ToList(); + + Params = Params + .Concat(targetToExtend.Params) + .GroupBy(p => p.Param) + .Select(g => new DefinedParam(g.Key, g.Any(p => p.Required))) + .ToList(); + + ConsumedArtifacts = ConsumedArtifacts + .Concat(targetToExtend.ConsumedArtifacts) + .Distinct() + .ToList(); + + ProducedArtifacts = ProducedArtifacts + .Concat(targetToExtend.ProducedArtifacts) + .Distinct() + .ToList(); + + ConsumedVariables = ConsumedVariables + .Concat(targetToExtend.ConsumedVariables) + .Distinct() + .ToList(); + + ProducedVariables = ProducedVariables + .Concat(targetToExtend.ProducedVariables) + .Distinct() + .ToList(); } else { @@ -108,31 +133,38 @@ internal TargetDefinition ApplyExtensions(IBuildDefinition buildDefinition) Dependencies = targetToExtend .Dependencies .Concat(Dependencies) + .Distinct() .ToList(); Params = targetToExtend .Params .Concat(Params) + .GroupBy(p => p.Param) + .Select(g => new DefinedParam(g.Key, g.Any(p => p.Required))) .ToList(); ConsumedArtifacts = targetToExtend .ConsumedArtifacts .Concat(ConsumedArtifacts) + .Distinct() .ToList(); ProducedArtifacts = targetToExtend .ProducedArtifacts .Concat(ProducedArtifacts) + .Distinct() .ToList(); ConsumedVariables = targetToExtend .ConsumedVariables .Concat(ConsumedVariables) + .Distinct() .ToList(); ProducedVariables = targetToExtend .ProducedVariables .Concat(ProducedVariables) + .Distinct() .ToList(); } } @@ -212,6 +244,7 @@ public TargetDefinition Executes(Action action) /// The current for fluent chaining. public TargetDefinition DependsOn(string targetName) { + ArgumentException.ThrowIfNullOrWhiteSpace(targetName); Dependencies.Add(targetName); return this; @@ -227,8 +260,11 @@ public TargetDefinition DependsOn(string targetName) public TargetDefinition DependsOn(Target target, [CallerArgumentExpression("target")] string? targetName = null) { if (string.IsNullOrWhiteSpace(targetName)) - throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + throw new ArgumentException(""" + Unable to infer target name from argument expression. + This usually happens when passing a target through a variable. + Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn("MyTarget") + """, nameof(target)); Dependencies.Add(targetName); @@ -239,11 +275,11 @@ public TargetDefinition DependsOn(Target target, [CallerArgumentExpression("targ /// /// Adds a dependency on another target. /// - /// The workflow target definition to depend on. + /// The workflow target definition to depend on. /// The current for fluent chaining. - public TargetDefinition DependsOn(WorkflowTargetDefinition workflowTarget) + public TargetDefinition DependsOn(TargetDefinition target) { - Dependencies.Add(workflowTarget.Name); + Dependencies.Add(target.Name); return this; } @@ -255,6 +291,7 @@ public TargetDefinition DependsOn(WorkflowTargetDefinition workflowTarget) /// The current for fluent chaining. public TargetDefinition UsesParam(params IEnumerable paramNames) { + ArgumentNullException.ThrowIfNull(paramNames); Params.AddRange(paramNames.Select(x => new DefinedParam(x, false))); return this; @@ -267,6 +304,7 @@ public TargetDefinition UsesParam(params IEnumerable paramNames) /// The current for fluent chaining. public TargetDefinition RequiresParam(params IEnumerable paramNames) { + ArgumentNullException.ThrowIfNull(paramNames); Params.AddRange(paramNames.Select(x => new DefinedParam(x, true))); return this; @@ -372,7 +410,7 @@ public TargetDefinition ConsumesArtifacts( /// The name of the variable. /// The current for fluent chaining. /// - /// This only declares the variable; it must be written using . + /// This only declares the variable; it must be written using . /// public TargetDefinition ProducesVariable(string variableName) { diff --git a/DecSm.Atom/Build/Definition/TargetDefinitionExtensions.cs b/src/Invex.Atom.Build/Definition/TargetDefinitionExtensions.cs similarity index 68% rename from DecSm.Atom/Build/Definition/TargetDefinitionExtensions.cs rename to src/Invex.Atom.Build/Definition/TargetDefinitionExtensions.cs index a4ccf214..ad2c3a74 100644 --- a/DecSm.Atom/Build/Definition/TargetDefinitionExtensions.cs +++ b/src/Invex.Atom.Build/Definition/TargetDefinitionExtensions.cs @@ -1,8 +1,9 @@ -namespace DecSm.Atom.Build.Definition; +namespace Invex.Atom.Build.Definition; /// /// Provides extension methods for to simplify adding multiple dependencies. /// +[PublicAPI] public static class TargetDefinitionExtensions { extension(TargetDefinition targetDefinition) @@ -20,12 +21,12 @@ public TargetDefinition DependsOn( { if (string.IsNullOrWhiteSpace(target1Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target1)); if (string.IsNullOrWhiteSpace(target2Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target2)); return targetDefinition @@ -48,17 +49,17 @@ public TargetDefinition DependsOn( { if (string.IsNullOrWhiteSpace(target1Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target1)); if (string.IsNullOrWhiteSpace(target2Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target2)); if (string.IsNullOrWhiteSpace(target3Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target3)); return targetDefinition @@ -84,22 +85,22 @@ public TargetDefinition DependsOn( { if (string.IsNullOrWhiteSpace(target1Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target1)); if (string.IsNullOrWhiteSpace(target2Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target2)); if (string.IsNullOrWhiteSpace(target3Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target3)); if (string.IsNullOrWhiteSpace(target4Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target4)); return targetDefinition @@ -128,27 +129,27 @@ public TargetDefinition DependsOn( { if (string.IsNullOrWhiteSpace(target1Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target1)); if (string.IsNullOrWhiteSpace(target2Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target2)); if (string.IsNullOrWhiteSpace(target3Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target3)); if (string.IsNullOrWhiteSpace(target4Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target4)); if (string.IsNullOrWhiteSpace(target5Name)) throw new ArgumentException( - "Unable to infer target name from argument expression. Please use DependsOn(\"TargetName\") overload.", + "Unable to infer target name from argument expression. This usually happens when passing a target through a variable. Instead of: var t = Build.MyTarget; DependsOn(t), use: DependsOn(nameof(IMyTargets.MyTarget)) or DependsOn(\"TargetName\")", nameof(target5)); return targetDefinition diff --git a/src/Invex.Atom.Build/Exceptions/AtomException.cs b/src/Invex.Atom.Build/Exceptions/AtomException.cs new file mode 100644 index 00000000..46dd95b9 --- /dev/null +++ b/src/Invex.Atom.Build/Exceptions/AtomException.cs @@ -0,0 +1,31 @@ +namespace Invex.Atom.Build.Exceptions; + +/// +/// Represents the base exception class for all Atom framework-specific exceptions. +/// +[PublicAPI] +[Serializable] +public class AtomException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public AtomException() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public AtomException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or null if no inner exception is + /// specified. + /// + public AtomException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/src/Invex.Atom.Build/Exceptions/BuildConfigurationException.cs b/src/Invex.Atom.Build/Exceptions/BuildConfigurationException.cs new file mode 100644 index 00000000..f9be7ff6 --- /dev/null +++ b/src/Invex.Atom.Build/Exceptions/BuildConfigurationException.cs @@ -0,0 +1,107 @@ +namespace Invex.Atom.Build.Exceptions; + +/// +/// Thrown when the build configuration contains errors such as duplicate targets, missing dependencies, or circular +/// dependencies. +/// +/// +/// +/// This exception is thrown during build model resolution when the detects +/// configuration errors that prevent the build from proceeding. Common scenarios include: +/// +/// +/// +/// Duplicate target names defined in the build +/// +/// +/// A target depends on a non-existent target +/// +/// +/// Circular dependencies between targets +/// +/// +/// +/// The exception may include a property with enhanced error details +/// that can be rendered for better visualization of the configuration problem. +/// +/// +/// +/// Example 1: Duplicate target error with ListReportData +/// +/// throw new BuildConfigurationException("One or more targets are defined multiple times.") +/// { +/// ReportData = new ListReportData(new[] { "Target 'Build' is defined multiple times" }) +/// { +/// Title = "Duplicate Targets" +/// } +/// }; +/// +/// Example 2: Circular dependency error with TextReportData +/// +/// throw new BuildConfigurationException("Circular dependency detected: Build -> Test -> Build") +/// { +/// ReportData = new TextReportData("Dependency cycle:\n Build -> Test -> Build") +/// { +/// Title = "Circular Dependency Detected" +/// } +/// }; +/// +/// Example 3: Missing dependency error +/// +/// throw new BuildConfigurationException("Target 'Build' depends on target 'Compile' which does not exist."); +/// +/// Example 4: How to catch and handle +/// +/// try +/// { +/// var buildModel = buildResolver.Resolve(); +/// } +/// catch (BuildConfigurationException ex) +/// { +/// Console.WriteLine($"Configuration error: {ex.Message}"); +/// if (ex.ReportData is not null) +/// { +/// reportService.AddReportData(ex.ReportData); +/// } +/// } +/// +/// +/// +/// +[PublicAPI] +[Serializable] +public class BuildConfigurationException : AtomException +{ + /// + /// Initializes a new instance of the class. + /// + public BuildConfigurationException() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public BuildConfigurationException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or null if no inner exception is + /// specified. + /// + public BuildConfigurationException(string message, Exception innerException) : base(message, innerException) { } + + /// + /// Gets optional enhanced error details that can be rendered to provide better visualization of the configuration + /// problem. + /// + /// + /// This property can contain for listing multiple issues, + /// or for formatted text output, or any other + /// implementation. + /// + public ICustomReportData? ReportData { get; init; } +} diff --git a/src/Invex.Atom.Build/Exceptions/CommandLineException.cs b/src/Invex.Atom.Build/Exceptions/CommandLineException.cs new file mode 100644 index 00000000..7deb55a1 --- /dev/null +++ b/src/Invex.Atom.Build/Exceptions/CommandLineException.cs @@ -0,0 +1,100 @@ +namespace Invex.Atom.Build.Exceptions; + +/// +/// Thrown when command-line arguments are invalid or malformed. +/// +/// +/// +/// This exception is thrown when the command-line argument parser detects invalid or malformed arguments. +/// Common scenarios include: +/// +/// +/// +/// Missing parameter values (e.g., --project without a path) +/// +/// +/// Invalid argument syntax (e.g., parameter value that looks like another option) +/// +/// +/// Unknown parameters or flags +/// +/// +/// +/// The property, when set, identifies the specific command-line argument +/// that caused the error, making it easier to diagnose and fix the issue. +/// +/// +/// For help with command-line usage, run the build with the --help flag. +/// +/// +/// +/// Example 1: Missing parameter value +/// +/// throw new CommandLineException("Missing value for --project option. Usage: --project <path>") +/// { +/// ArgumentName = "project" +/// }; +/// +/// Example 2: Invalid argument syntax +/// +/// throw new CommandLineException("Missing value for parameter 'output'. The next argument '--verbose' looks like another option. Usage: --output <value>") +/// { +/// ArgumentName = "output" +/// }; +/// +/// Example 3: How to catch and provide user guidance +/// +/// try +/// { +/// var args = commandLineArgsParser.Parse(commandLineArgs); +/// } +/// catch (CommandLineException ex) +/// { +/// Console.WriteLine($"Invalid arguments: {ex.Message}"); +/// if (ex.ArgumentName is not null) +/// { +/// Console.WriteLine($"Problematic argument: {ex.ArgumentName}"); +/// } +/// Console.WriteLine("Run with --help for usage information."); +/// Environment.ExitCode = 2; +/// } +/// +/// +/// +/// +[PublicAPI] +[Serializable] +public class CommandLineException : AtomException +{ + /// + /// Initializes a new instance of the class. + /// + public CommandLineException() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public CommandLineException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or null if no inner exception is + /// specified. + /// + public CommandLineException(string message, Exception innerException) : base(message, innerException) { } + + /// + /// Gets the name of the command-line argument that caused the error, if applicable. + /// + /// + /// This property helps identify which specific argument was problematic, making it easier to diagnose + /// and fix command-line issues. For example, if the --project option is missing its value, + /// this property would be set to "project". + /// + public string? ArgumentName { get; init; } +} diff --git a/DecSm.Atom/StepFailedException.cs b/src/Invex.Atom.Build/Exceptions/StepFailedException.cs similarity index 68% rename from DecSm.Atom/StepFailedException.cs rename to src/Invex.Atom.Build/Exceptions/StepFailedException.cs index a995b30b..10a554ff 100644 --- a/DecSm.Atom/StepFailedException.cs +++ b/src/Invex.Atom.Build/Exceptions/StepFailedException.cs @@ -1,17 +1,25 @@ -namespace DecSm.Atom; +namespace Invex.Atom.Build.Exceptions; /// /// Represents an exception thrown when a step within an Atom build target fails. /// This exception provides additional reporting capabilities through custom report data. /// /// -/// This exception is used throughout the Atom framework to signal failures in build targets, -/// such as task execution errors, failed external process calls (via ), -/// or unmet validation criteria within a target's logic. -/// Consumers of the Atom framework can also throw StepFailedException to programmatically halt a build target -/// and optionally provide custom report data using the property. -/// The property allows attaching custom reporting information that can be -/// used for enhanced error reporting, logging, or debugging purposes by Atom's reporting services. +/// +/// This exception is used throughout the Atom framework to signal failures in build targets, +/// such as task execution errors, failed external process calls (via ), +/// or unmet validation criteria within a target's logic. +/// Consumers of the Atom framework can also throw StepFailedException to programmatically halt a build +/// target +/// and optionally provide custom report data using the property. +/// The property allows attaching custom reporting information that can be +/// used for enhanced error reporting, logging, or debugging purposes by Atom's reporting services. +/// +/// +/// This exception inherits from . Use for +/// configuration errors (duplicate targets, circular dependencies); use for +/// runtime target execution failures. +/// /// /// /// @@ -33,9 +41,11 @@ /// } /// /// +/// +/// [PublicAPI] public sealed class StepFailedException(string message, Exception? innerException = null) - : Exception(message, innerException) + : AtomException(message, innerException!) { /// /// Initializes a new instance of the class with an empty message. diff --git a/src/Invex.Atom.Build/FileSystem/AtomPathProvider.cs b/src/Invex.Atom.Build/FileSystem/AtomPathProvider.cs new file mode 100644 index 00000000..b767a9a2 --- /dev/null +++ b/src/Invex.Atom.Build/FileSystem/AtomPathProvider.cs @@ -0,0 +1,47 @@ +namespace Invex.Atom.Build.FileSystem; + +[PublicAPI] +internal sealed class AtomPathProvider(IServiceProvider services) : IPathProvider +{ + private IRootedFileSystem RootedFileSystem => field ??= services.GetRequiredService(); + + public int Priority => 0; + + public RootedPath? GetPath(string key) => + key switch + { + AtomPaths.Root => GetRoot(), + AtomPaths.Artifacts or AtomPaths.Publish => RootedFileSystem.GetPath(AtomPaths.Root) / "atom-publish", + AtomPaths.Temp => new(RootedFileSystem, RootedFileSystem.Path.GetTempPath()), + _ => null, + }; + + /// + /// Determines the root directory by traversing up from the current directory and looking for project markers. + /// + private RootedPath GetRoot() + { + var currentDir = RootedFileSystem.CurrentDirectory; + + while (currentDir.Parent is not null) + { + currentDir = currentDir.Parent; + + if (RootedFileSystem + .Directory + .EnumerateDirectories(currentDir, "*.git", SearchOption.TopDirectoryOnly) + .Any() || + RootedFileSystem + .Directory + .EnumerateFiles(currentDir, "*.slnx", SearchOption.TopDirectoryOnly) + .Any() || + RootedFileSystem + .Directory + .EnumerateFiles(currentDir, "*.sln", SearchOption.TopDirectoryOnly) + .Any()) + return currentDir; + } + + return RootedFileSystem.CurrentDirectory; + } +} diff --git a/src/Invex.Atom.Build/FileSystem/AtomPaths.cs b/src/Invex.Atom.Build/FileSystem/AtomPaths.cs new file mode 100644 index 00000000..82d90081 --- /dev/null +++ b/src/Invex.Atom.Build/FileSystem/AtomPaths.cs @@ -0,0 +1,28 @@ +namespace Invex.Atom.Build.FileSystem; + +/// +/// Provides constants for key directory paths and an extension method for path provider registration. +/// +[PublicAPI] +public static class AtomPaths +{ + /// + /// Represents the key for the root directory of the Atom project. + /// + public const string Root = "Root"; + + /// + /// Represents the key for the directory where build artifacts are stored. + /// + public const string Artifacts = "Artifacts"; + + /// + /// Represents the key for the directory where build outputs are published. + /// + public const string Publish = "Publish"; + + /// + /// Represents the key for the temporary directory used during builds. + /// + public const string Temp = "Temp"; +} diff --git a/src/Invex.Atom.Build/FileSystem/FileSystemExtensions.cs b/src/Invex.Atom.Build/FileSystem/FileSystemExtensions.cs new file mode 100644 index 00000000..ca6a34bb --- /dev/null +++ b/src/Invex.Atom.Build/FileSystem/FileSystemExtensions.cs @@ -0,0 +1,33 @@ +namespace Invex.Atom.Build.FileSystem; + +[PublicAPI] +public static class FileSystemExtensions +{ + extension(IRootedFileSystem fileSystem) + { + /// + /// Gets the root directory of the Atom project, identified by markers like .git or .sln files. + /// + public RootedPath AtomRootDirectory => fileSystem.GetPath(AtomPaths.Root); + + /// + /// Gets the default directory for storing build artifacts. + /// + public RootedPath AtomArtifactsDirectory => fileSystem.GetPath(AtomPaths.Artifacts); + + /// + /// Gets the default directory for publishing final build outputs. + /// + public RootedPath AtomPublishDirectory => fileSystem.GetPath(AtomPaths.Publish); + + /// + /// Gets the temporary directory for build operations. + /// + public RootedPath AtomTempDirectory => fileSystem.GetPath(AtomPaths.Temp); + + /// + /// Gets the current working directory of the application. + /// + public RootedPath CurrentDirectory => new(fileSystem, fileSystem.Directory.GetCurrentDirectory()); + } +} diff --git a/DecSm.Atom/Help/HelpService.cs b/src/Invex.Atom.Build/Help/HelpService.cs similarity index 98% rename from DecSm.Atom/Help/HelpService.cs rename to src/Invex.Atom.Build/Help/HelpService.cs index 1f6a8c75..00104e2d 100644 --- a/DecSm.Atom/Help/HelpService.cs +++ b/src/Invex.Atom.Build/Help/HelpService.cs @@ -1,8 +1,9 @@ -namespace DecSm.Atom.Help; +namespace Invex.Atom.Build.Help; /// /// Defines a service for displaying help information about the build system. /// +[PublicAPI] public interface IHelpService { /// @@ -47,8 +48,6 @@ public void ShowHelp() console.Write( new Markup(" [dim]-i, --interactive[/] [dim]Run in interactive mode (prompt for required params)[/]\n")); - console.Write(new Markup(" [dim]-g, --gen[/] [dim]Generate build scripts[/]\n")); - console.Write(new Markup( " [dim]-s, --skip[/] [dim]Skip dependency execution (run only specified commands)[/]\n")); @@ -105,6 +104,7 @@ public void ShowHelp() if (libraryTargets.Count > 0) { console.Write(new Markup("[bold]Library Commands[/]\n")); + console.Write(new Markup("[dim]Name (Alias) | Description[/]\n")); console.WriteLine(); foreach (var target in libraryTargets) @@ -115,6 +115,7 @@ public void ShowHelp() if (projectTargets.Count > 0) { console.Write(new Markup("[bold]Project Commands[/]\n")); + console.Write(new Markup("[dim]Name (Alias) | Description[/]\n")); console.WriteLine(); foreach (var target in projectTargets) diff --git a/DecSm.Atom/Hosting/AtomHost.cs b/src/Invex.Atom.Build/Hosting/AtomHost.cs similarity index 85% rename from DecSm.Atom/Hosting/AtomHost.cs rename to src/Invex.Atom.Build/Hosting/AtomHost.cs index 33b62f9d..2bd9306d 100644 --- a/DecSm.Atom/Hosting/AtomHost.cs +++ b/src/Invex.Atom.Build/Hosting/AtomHost.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Hosting; +namespace Invex.Atom.Build.Hosting; /// /// Provides static methods for creating and running an Atom host application. @@ -13,12 +13,12 @@ public static class AtomHost /// /// Creates and configures a for an Atom application. /// - /// The type to configure the host with. + /// The type to configure the host with. /// The command-line arguments for the application. /// A configured ready for further customization or building. public static HostApplicationBuilder CreateAtomBuilder< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>(string[] args) - where T : MinimalBuildDefinition + where T : BuildDefinition { var builder = Host.CreateEmptyApplicationBuilder(new() { @@ -44,14 +44,14 @@ public static HostApplicationBuilder CreateAtomBuilder< /// /// Builds and runs an Atom application using the specified build definition. /// - /// The type to configure and run the application with. + /// The type to configure and run the application with. /// The command-line arguments for the application. /// /// This method handles the complete setup, build, and execution lifecycle of the host. /// public static void Run<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>( string[] args) - where T : MinimalBuildDefinition => + where T : BuildDefinition => CreateAtomBuilder(args) .Build() .UseAtom() diff --git a/DecSm.Atom/Hosting/ConfigureHostAttribute.cs b/src/Invex.Atom.Build/Hosting/ConfigureHostAttribute.cs similarity index 96% rename from DecSm.Atom/Hosting/ConfigureHostAttribute.cs rename to src/Invex.Atom.Build/Hosting/ConfigureHostAttribute.cs index 6d40ae8d..718061a8 100644 --- a/DecSm.Atom/Hosting/ConfigureHostAttribute.cs +++ b/src/Invex.Atom.Build/Hosting/ConfigureHostAttribute.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Hosting; +namespace Invex.Atom.Build.Hosting; /// /// Marks an interface as a source for host configuration, triggering source generation to apply its logic. diff --git a/DecSm.Atom/Hosting/ConfigureHostBuilderAttribute.cs b/src/Invex.Atom.Build/Hosting/ConfigureHostBuilderAttribute.cs similarity index 97% rename from DecSm.Atom/Hosting/ConfigureHostBuilderAttribute.cs rename to src/Invex.Atom.Build/Hosting/ConfigureHostBuilderAttribute.cs index 16425694..e040064e 100644 --- a/DecSm.Atom/Hosting/ConfigureHostBuilderAttribute.cs +++ b/src/Invex.Atom.Build/Hosting/ConfigureHostBuilderAttribute.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Hosting; +namespace Invex.Atom.Build.Hosting; /// /// Marks an interface as a source for host builder configuration, triggering source generation to apply its logic. diff --git a/DecSm.Atom/Hosting/GenerateEntryPointAttribute.cs b/src/Invex.Atom.Build/Hosting/GenerateEntryPointAttribute.cs similarity index 86% rename from DecSm.Atom/Hosting/GenerateEntryPointAttribute.cs rename to src/Invex.Atom.Build/Hosting/GenerateEntryPointAttribute.cs index ca868777..f3b9652d 100644 --- a/DecSm.Atom/Hosting/GenerateEntryPointAttribute.cs +++ b/src/Invex.Atom.Build/Hosting/GenerateEntryPointAttribute.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Hosting; +namespace Invex.Atom.Build.Hosting; /// /// Triggers the source generation of the application's entry point (Main method). @@ -16,5 +16,5 @@ /// /// [PublicAPI] -[AttributeUsage(AttributeTargets.Class)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)] public sealed class GenerateEntryPointAttribute : Attribute; diff --git a/DecSm.Atom/Hosting/HostExtensions.cs b/src/Invex.Atom.Build/Hosting/HostExtensions.cs similarity index 65% rename from DecSm.Atom/Hosting/HostExtensions.cs rename to src/Invex.Atom.Build/Hosting/HostExtensions.cs index d875a1cf..ec642259 100644 --- a/DecSm.Atom/Hosting/HostExtensions.cs +++ b/src/Invex.Atom.Build/Hosting/HostExtensions.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Hosting; +namespace Invex.Atom.Build.Hosting; /// /// Provides extension methods for configuring and integrating Atom services into a .NET host. @@ -15,7 +15,7 @@ public static class HostExtensions /// logging, and default settings. /// /// The type of the host application builder. - /// The type of the build definition, which must implement . + /// The type of the build definition, which must implement . /// The host application builder instance to configure. /// The command-line arguments provided to the application. /// The configured host application builder instance. @@ -32,7 +32,7 @@ public static class HostExtensions /// Registers default providers for build information: , /// , and . /// - /// Configures file system access via . + /// Configures file system access via . /// Registers build execution and workflow generation services. /// Sets up logging with Spectre.Console and report providers, filtering out verbose Microsoft host logs. /// Parses command-line arguments and makes them available as . @@ -45,66 +45,68 @@ public static class HostExtensions this TBuilder builder, string[] args) where TBuilder : IHostApplicationBuilder - where TBuild : MinimalBuildDefinition + where TBuild : BuildDefinition { + builder + .Services + .AddOptions() + .Configure(o => + { + o.ValidateScopes = true; + o.ValidateOnBuild = true; + }); + builder.Services.AddHostedService(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(x => x.GetRequiredService()); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(x => x.GetRequiredService()); // ReSharper disable once SuspiciousTypeConversion.Global - Checked before casting if (typeof(TBuild).IsAssignableTo(typeof(IConfigureHost))) - builder.Services.AddSingleton(x => - (IConfigureHost)x.GetRequiredService()); + builder.Services.AddSingleton(x => (IConfigureHost)x.GetRequiredService()); - builder.Services.AddSingletonWithStaticAccessor(); - builder.Services.AddSingletonWithStaticAccessor(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddSingletonWithStaticAccessor((_, _) => + builder.Services.AddSingleton(services => { // Wrap Spectre's console output so all rendered text is masked for secrets before being written - var innerOutput = AnsiConsole.Console.Profile.Out; var settings = new AnsiConsoleSettings { - Out = new MaskingAnsiConsoleOutput(innerOutput), + Out = new MaskingAnsiConsoleOutput(AnsiConsole.Console.Profile.Out, + services.GetRequiredService()), }; return AnsiConsole.Create(settings); }); builder.Services.AddSingleton(TimeProvider.System); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder .Services - .AddKeyedSingleton("RootFileSystem", new FileSystem()) - .AddSingletonWithStaticAccessor((x, _) => - new AtomFileSystem(x.GetRequiredService>()) - { - FileSystem = x.GetRequiredKeyedService("RootFileSystem"), - PathLocators = x - .GetServices() - .OrderByDescending(l => l.Priority) - .ToList(), - ProjectName = x.GetRequiredService() - .ProjectName is { Length: > 0 } p - ? p - : Assembly.GetEntryAssembly()!.GetName() - .Name!, - }) - .AddSingletonWithStaticAccessor((x, _) => x.GetRequiredService()); + .AddRootedFileSystem() + .AddSingleton(); + + builder.Services.AddSingleton(services => new AtomProjectData + { + ProjectName = services.GetRequiredService() + .ProjectName is { Length: > 0 } p + ? p + : Assembly.GetEntryAssembly()!.GetName() + .Name!, + IsFileBasedApp = AppContext.GetData("EntryPointFilePath") is string s && s.EndsWith(".cs"), + }); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddProcessRunner(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); + builder.Services.AddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.AddSingleton(); @@ -121,18 +123,39 @@ public static class HostExtensions return parsedArgs; }); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(services => services - .GetRequiredService() - .Resolve()); + builder.Services.AddSingleton(services => + { + try + { + return services + .GetRequiredService() + .Resolve(); + } + catch (BuildConfigurationException ex) + { + // Log and re-throw to prevent app startup + var logger = services.GetService>(); + logger?.LogError("Build configuration error during initialization: {Message}", ex.Message); + + throw; + } + catch (Exception ex) + { + var logger = services.GetService>(); + logger?.LogError(ex, "Unexpected error during build model resolution"); + + throw; + } + }); builder.Logging.ClearProviders(); builder.Logging.SetMinimumLevel(LogLevel.Trace); - builder.Logging.AddProvider(new SpectreLoggerProvider()); - builder.Logging.AddProvider(new ReportLoggerProvider()); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Logging.AddFilter((context, level) => (context, level) switch { @@ -144,6 +167,10 @@ public static class HostExtensions using var tempBuild = builder.Services.BuildServiceProvider(); + tempBuild + .GetRequiredService() + .ConfigureDefinitionHost(builder); + tempBuild .GetService() ?.ConfigureBuildHostBuilder(builder); diff --git a/DecSm.Atom/Hosting/IConfigureHost.cs b/src/Invex.Atom.Build/Hosting/IConfigureHost.cs similarity index 95% rename from DecSm.Atom/Hosting/IConfigureHost.cs rename to src/Invex.Atom.Build/Hosting/IConfigureHost.cs index 5ece37d1..990f4182 100644 --- a/DecSm.Atom/Hosting/IConfigureHost.cs +++ b/src/Invex.Atom.Build/Hosting/IConfigureHost.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Hosting; +namespace Invex.Atom.Build.Hosting; /// /// Defines methods for customizing the host configuration during application startup. @@ -9,6 +9,7 @@ /// It provides hooks for modifying the host builder and the host itself. This interface is intended for internal /// framework use. /// +[PublicAPI] public interface IConfigureHost { /// diff --git a/src/Invex.Atom.Build/IAtomLifecycleHook.cs b/src/Invex.Atom.Build/IAtomLifecycleHook.cs new file mode 100644 index 00000000..e49e52ea --- /dev/null +++ b/src/Invex.Atom.Build/IAtomLifecycleHook.cs @@ -0,0 +1,56 @@ +namespace Invex.Atom.Build; + +/// +/// Defines a hook that can participate in the Atom build lifecycle. +/// +/// +/// +/// Implementations of this interface can be registered in the dependency injection container +/// to receive callbacks at specific points in the Atom build lifecycle. Multiple hooks can +/// be registered and will be invoked in registration order. +/// +/// +/// All methods have default no-op implementations, so implementers only need to override the +/// methods they are interested in. +/// +/// +/// +/// +/// public class MyLifecycleHook : IAtomLifecycleHook +/// { +/// public Task BeforeExecute(CancellationToken cancellationToken) +/// { +/// // Check preconditions before targets are executed +/// return Task.CompletedTask; +/// } +/// } +/// +/// +[PublicAPI] +public interface IAtomLifecycleHook +{ + /// + /// Called after the build model has been resolved and before build targets are executed. + /// + /// + /// This is an ideal point for modules to perform validation or generate files + /// that must be up-to-date before the build proceeds (e.g., workflow generation checks). + /// Throwing an exception from this method will prevent the build from executing. + /// + /// A cancellation token. + /// A task representing the asynchronous operation. + Task BeforeExecute(CancellationToken cancellationToken) => + Task.CompletedTask; + + /// + /// Called after all build targets have completed execution (regardless of success or failure). + /// + /// + /// This hook is called in a finally-like manner, allowing modules to perform cleanup or + /// post-execution processing. + /// + /// A cancellation token. + /// A task representing the asynchronous operation. + Task AfterExecute(CancellationToken cancellationToken) => + Task.CompletedTask; +} diff --git a/DecSm.Atom/Build/IBuildAccessor.cs b/src/Invex.Atom.Build/IBuildAccessor.cs similarity index 96% rename from DecSm.Atom/Build/IBuildAccessor.cs rename to src/Invex.Atom.Build/IBuildAccessor.cs index 2fa3b572..e0de4173 100644 --- a/DecSm.Atom/Build/IBuildAccessor.cs +++ b/src/Invex.Atom.Build/IBuildAccessor.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build; +namespace Invex.Atom.Build; /// /// Provides base functionality for accessing services and parameters within the Atom build system. @@ -25,7 +25,7 @@ public interface IBuildAccessor /// /// Gets the Atom file system service for file and directory operations. /// - protected IAtomFileSystem FileSystem => GetService(); + protected IRootedFileSystem RootedFileSystem => GetService(); /// /// Gets the process runner service for executing external processes. diff --git a/DecSm.Atom/ISetupBuildInfo.cs b/src/Invex.Atom.Build/ISetupBuildInfo.cs similarity index 93% rename from DecSm.Atom/ISetupBuildInfo.cs rename to src/Invex.Atom.Build/ISetupBuildInfo.cs index 5e39bbe8..108e288b 100644 --- a/DecSm.Atom/ISetupBuildInfo.cs +++ b/src/Invex.Atom.Build/ISetupBuildInfo.cs @@ -1,6 +1,9 @@ -namespace DecSm.Atom; +namespace Invex.Atom.Build; -/// See +/// +/// See +/// +[PublicAPI] public interface ISetupBuildInfo : IBuildInfo, IVariablesHelper, IReportsHelper { /// @@ -20,13 +23,13 @@ public interface ISetupBuildInfo : IBuildInfo, IVariablesHelper, IReportsHelper /// /// /// - /// Retrieves the build name (from ) and writes it as a build variable + /// Retrieves the build ID (from ) and writes it as a build variable /// named "BuildId". /// /// /// /// - /// Retrieves the build name (from ) and writes it as a build + /// Retrieves the build version (from ) and writes it as a build /// variable /// named /// "BuildVersion". @@ -34,7 +37,8 @@ public interface ISetupBuildInfo : IBuildInfo, IVariablesHelper, IReportsHelper /// /// /// - /// Retrieves the build name (from ), converts it to a string, + /// Retrieves the build timestamp (from ), converts it to a + /// string, /// and /// writes it /// as a build variable named "BuildTimestamp". diff --git a/DecSm.Atom/IValidateBuild.cs b/src/Invex.Atom.Build/IValidateBuild.cs similarity index 98% rename from DecSm.Atom/IValidateBuild.cs rename to src/Invex.Atom.Build/IValidateBuild.cs index 9703afbc..2177e59c 100644 --- a/DecSm.Atom/IValidateBuild.cs +++ b/src/Invex.Atom.Build/IValidateBuild.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom; +namespace Invex.Atom.Build; /// /// Defines a target responsible for validating the Atom build configuration. diff --git a/DecSm.Atom/DecSm.Atom.csproj b/src/Invex.Atom.Build/Invex.Atom.Build.csproj similarity index 52% rename from DecSm.Atom/DecSm.Atom.csproj rename to src/Invex.Atom.Build/Invex.Atom.Build.csproj index 025245d9..b19ea997 100644 --- a/DecSm.Atom/DecSm.Atom.csproj +++ b/src/Invex.Atom.Build/Invex.Atom.Build.csproj @@ -5,10 +5,19 @@ + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -17,8 +26,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -26,16 +33,15 @@ - - - - + + - - - + + + + \ No newline at end of file diff --git a/src/Invex.Atom.Build/Invex.Atom.Build.props b/src/Invex.Atom.Build/Invex.Atom.Build.props new file mode 100644 index 00000000..3e82ec89 --- /dev/null +++ b/src/Invex.Atom.Build/Invex.Atom.Build.props @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Invex.Atom.Build/Invex.Atom.Build.targets b/src/Invex.Atom.Build/Invex.Atom.Build.targets new file mode 100644 index 00000000..eb2d9337 --- /dev/null +++ b/src/Invex.Atom.Build/Invex.Atom.Build.targets @@ -0,0 +1,9 @@ + + + + + + diff --git a/DecSm.Atom/Logging/LogOptions.cs b/src/Invex.Atom.Build/Logging/LogOptions.cs similarity index 95% rename from DecSm.Atom/Logging/LogOptions.cs rename to src/Invex.Atom.Build/Logging/LogOptions.cs index 723a8857..44bc096a 100644 --- a/DecSm.Atom/Logging/LogOptions.cs +++ b/src/Invex.Atom.Build/Logging/LogOptions.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Logging; +namespace Invex.Atom.Build.Logging; /// /// Provides static options for controlling logging behavior across the application. diff --git a/DecSm.Atom/Logging/MaskingAnsiConsoleOutput.cs b/src/Invex.Atom.Build/Logging/MaskingAnsiConsoleOutput.cs similarity index 67% rename from DecSm.Atom/Logging/MaskingAnsiConsoleOutput.cs rename to src/Invex.Atom.Build/Logging/MaskingAnsiConsoleOutput.cs index f7c45000..a64f6e94 100644 --- a/DecSm.Atom/Logging/MaskingAnsiConsoleOutput.cs +++ b/src/Invex.Atom.Build/Logging/MaskingAnsiConsoleOutput.cs @@ -1,37 +1,26 @@ -namespace DecSm.Atom.Logging; +namespace Invex.Atom.Build.Logging; /// /// An wrapper that masks secret values before writing to the underlying output. /// -internal sealed class MaskingAnsiConsoleOutput : IAnsiConsoleOutput +internal sealed class MaskingAnsiConsoleOutput(IAnsiConsoleOutput inner, IServiceProvider serviceProvider) + : IAnsiConsoleOutput { - private readonly IAnsiConsoleOutput _inner; - - /// - /// Initializes a new instance of the class. - /// - /// The inner to wrap. - public MaskingAnsiConsoleOutput(IAnsiConsoleOutput inner) - { - _inner = inner; - Writer = new MaskingTextWriter(inner.Writer); - } - /// - public int Width => _inner.Width; + public int Width => inner.Width; /// - public int Height => _inner.Height; + public int Height => inner.Height; /// - public TextWriter Writer { get; } + public TextWriter Writer => field ??= new MaskingTextWriter(inner.Writer, serviceProvider); /// - public bool IsTerminal => _inner.IsTerminal; + public bool IsTerminal => inner.IsTerminal; /// public void SetEncoding(Encoding encoding) => - _inner.SetEncoding(encoding); + inner.SetEncoding(encoding); /// /// A that masks secrets before writing to an inner writer. @@ -39,16 +28,21 @@ public void SetEncoding(Encoding encoding) => private sealed class MaskingTextWriter : TextWriter { private readonly TextWriter _innerWriter; + private readonly IServiceProvider _serviceProvider; /// /// Initializes a new instance of the class. /// /// The inner to wrap. - public MaskingTextWriter(TextWriter innerWriter) + /// The instance. + public MaskingTextWriter(TextWriter innerWriter, IServiceProvider serviceProvider) { _innerWriter = innerWriter; + _serviceProvider = serviceProvider; } + private IParamService ParamService => field ??= _serviceProvider.GetRequiredService(); + /// public override Encoding Encoding => _innerWriter.Encoding; @@ -63,7 +57,7 @@ public override void Write(char value) => public override void Write(string? value) { if (value is { Length: > 0 }) - value = ServiceStaticAccessor.Service?.MaskMatchingSecrets(value) ?? value; + value = ParamService.MaskMatchingSecrets(value); _innerWriter.Write(value); } @@ -83,18 +77,12 @@ public override Task WriteAsync(char value) => /// Asynchronously masks secrets in the specified string and writes it to the inner writer. /// /// The string to write. - public override Task WriteAsync(string? value) - { - if (value is not { Length: > 0 }) - return _innerWriter.WriteAsync(value); - - var masker = ServiceStaticAccessor.Service; - - if (masker is not null) - value = masker.MaskMatchingSecrets(value); - - return _innerWriter.WriteAsync(value); - } + public override Task WriteAsync(string? value) => + value switch + { + null or { Length: 0 } => _innerWriter.WriteAsync(value), + _ => _innerWriter.WriteAsync(ParamService.MaskMatchingSecrets(value)), + }; /// public override Task WriteAsync(char[] buffer, int index, int count) diff --git a/DecSm.Atom/Logging/ReportLogger.cs b/src/Invex.Atom.Build/Logging/ReportLogger.cs similarity index 80% rename from DecSm.Atom/Logging/ReportLogger.cs rename to src/Invex.Atom.Build/Logging/ReportLogger.cs index 028cfc66..6c97458f 100644 --- a/DecSm.Atom/Logging/ReportLogger.cs +++ b/src/Invex.Atom.Build/Logging/ReportLogger.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Logging; +namespace Invex.Atom.Build.Logging; /// /// An internal logger implementation that captures log entries to be included in the final build report. @@ -7,9 +7,14 @@ /// This logger filters for significant events (Warning, Error, Critical) and sends them to the /// . It also masks any secrets found in the log messages. /// +/// The service provider for resolving services. /// The external scope provider for accessing scope data. -internal sealed class ReportLogger(IExternalScopeProvider? scopeProvider) : ILogger +internal sealed class ReportLogger(IServiceProvider serviceProvider, IExternalScopeProvider? scopeProvider) : ILogger { + private IParamService ParamService => field ??= serviceProvider.GetRequiredService(); + + private ReportService ReportService => field ??= serviceProvider.GetRequiredService(); + /// /// Checks if the given is enabled. /// @@ -67,10 +72,8 @@ public void Log( return; // If the message contains any secrets, we don't want to log it - message = ServiceStaticAccessor.Service?.MaskMatchingSecrets(message) ?? message; + message = ParamService.MaskMatchingSecrets(message); - ServiceStaticAccessor.Service?.AddReportData( - new LogReportData(message, exception, logLevel, time), - command); + ReportService.AddReportData(new LogReportData(message, exception, logLevel, time), command); } } diff --git a/DecSm.Atom/Logging/ReportLoggerProvider.cs b/src/Invex.Atom.Build/Logging/ReportLoggerProvider.cs similarity index 78% rename from DecSm.Atom/Logging/ReportLoggerProvider.cs rename to src/Invex.Atom.Build/Logging/ReportLoggerProvider.cs index 62d0e8a0..c397dc8b 100644 --- a/DecSm.Atom/Logging/ReportLoggerProvider.cs +++ b/src/Invex.Atom.Build/Logging/ReportLoggerProvider.cs @@ -1,9 +1,9 @@ -namespace DecSm.Atom.Logging; +namespace Invex.Atom.Build.Logging; /// /// An internal logger provider that creates instances of . /// -internal sealed class ReportLoggerProvider : ILoggerProvider +internal sealed class ReportLoggerProvider(IServiceProvider serviceProvider) : ILoggerProvider { private readonly ConcurrentDictionary _loggers = new(); private readonly LoggerExternalScopeProvider _scopeProvider = new(); @@ -14,7 +14,7 @@ internal sealed class ReportLoggerProvider : ILoggerProvider /// The category name for messages produced by the logger. /// A new instance. public ILogger CreateLogger(string categoryName) => - _loggers.GetOrAdd(categoryName, new ReportLogger(_scopeProvider)); + _loggers.GetOrAdd(categoryName, new ReportLogger(serviceProvider, _scopeProvider)); /// /// Disposes the provider and clears all cached logger instances. diff --git a/DecSm.Atom/Logging/SpectreLogger.cs b/src/Invex.Atom.Build/Logging/SpectreLogger.cs similarity index 91% rename from DecSm.Atom/Logging/SpectreLogger.cs rename to src/Invex.Atom.Build/Logging/SpectreLogger.cs index c0622a25..7bffb9e3 100644 --- a/DecSm.Atom/Logging/SpectreLogger.cs +++ b/src/Invex.Atom.Build/Logging/SpectreLogger.cs @@ -1,12 +1,21 @@ -namespace DecSm.Atom.Logging; +namespace Invex.Atom.Build.Logging; /// /// An internal logger implementation that writes formatted log messages to the console using Spectre.Console. /// /// The category name for the logger. +/// The service provider for resolving services. /// The external scope provider for accessing scope data. -internal sealed partial class SpectreLogger(string categoryName, IExternalScopeProvider? scopeProvider) : ILogger +internal sealed partial class SpectreLogger( + string categoryName, + IServiceProvider serviceProvider, + IExternalScopeProvider? scopeProvider +) : ILogger { + private IParamService ParamService => field ??= serviceProvider.GetRequiredService(); + + private IAnsiConsole AnsiConsole => field ??= serviceProvider.GetRequiredService(); + /// /// Checks if the given is enabled. /// @@ -123,7 +132,7 @@ public void Log( return; // If the message contains any secrets, we want to mask them - message = ServiceStaticAccessor.Service?.MaskMatchingSecrets(message) ?? message; + message = ParamService.MaskMatchingSecrets(message); message = message.EscapeMarkup(); @@ -139,7 +148,7 @@ public void Log( ? $"[{messageStyle}]{message}[/]" : message).LeftJustified()).Collapse(); - ServiceStaticAccessor.Service?.Write(columns); + AnsiConsole.Write(columns); return; } @@ -175,7 +184,7 @@ public void Log( table.AddRow(string.Empty); } - ServiceStaticAccessor.Service?.Write(table); + AnsiConsole.Write(table); } /// diff --git a/DecSm.Atom/Logging/SpectreLoggerProvider.cs b/src/Invex.Atom.Build/Logging/SpectreLoggerProvider.cs similarity index 83% rename from DecSm.Atom/Logging/SpectreLoggerProvider.cs rename to src/Invex.Atom.Build/Logging/SpectreLoggerProvider.cs index 6909281a..f16b842d 100644 --- a/DecSm.Atom/Logging/SpectreLoggerProvider.cs +++ b/src/Invex.Atom.Build/Logging/SpectreLoggerProvider.cs @@ -1,9 +1,9 @@ -namespace DecSm.Atom.Logging; +namespace Invex.Atom.Build.Logging; /// /// An internal logger provider that creates instances of . /// -internal sealed class SpectreLoggerProvider : ILoggerProvider +internal sealed class SpectreLoggerProvider(IServiceProvider serviceProvider) : ILoggerProvider { private readonly ConcurrentDictionary _loggers = new(); private readonly LoggerExternalScopeProvider _scopeProvider = new(); @@ -14,7 +14,7 @@ internal sealed class SpectreLoggerProvider : ILoggerProvider /// The category name for messages produced by the logger. /// A new instance. public ILogger CreateLogger(string categoryName) => - _loggers.GetOrAdd(categoryName, new SpectreLogger(categoryName, _scopeProvider)); + _loggers.GetOrAdd(categoryName, new SpectreLogger(categoryName, serviceProvider, _scopeProvider)); /// /// Disposes the provider and clears all cached logger instances. diff --git a/DecSm.Atom/Build/Model/BuildModel.cs b/src/Invex.Atom.Build/Model/BuildModel.cs similarity index 70% rename from DecSm.Atom/Build/Model/BuildModel.cs rename to src/Invex.Atom.Build/Model/BuildModel.cs index cb3dbda2..46df365b 100644 --- a/DecSm.Atom/Build/Model/BuildModel.cs +++ b/src/Invex.Atom.Build/Model/BuildModel.cs @@ -1,11 +1,11 @@ -namespace DecSm.Atom.Build.Model; +namespace Invex.Atom.Build.Model; /// /// Represents the complete model of a build, including all targets and their states. /// /// /// This model is generated by Atom on startup and serves as the primary source of information for the build. -/// It is immutable by design, with the exception of the values in . +/// It is immutable by design, except the values in . /// [PublicAPI] public sealed record BuildModel @@ -41,4 +41,16 @@ public sealed record BuildModel /// Thrown if no target with the specified name is found. public TargetModel GetTarget(string name) => Targets.FirstOrDefault(t => t.Name == name) ?? throw new ArgumentException($"Target '{name}' not found."); + + /// + /// Gets the state of a target. + /// + /// The target whose state to retrieve. + /// The for the specified target. + /// Thrown if the target is not present in the build model target states. + public TargetState GetTargetState(TargetModel target) => + TargetStates.TryGetValue(target, out var state) + ? state + : throw new InvalidOperationException( + $"Target '{target.Name}' is not present in build model target states."); } diff --git a/DecSm.Atom/Build/Model/ConsumedArtifact.cs b/src/Invex.Atom.Build/Model/ConsumedArtifact.cs similarity index 93% rename from DecSm.Atom/Build/Model/ConsumedArtifact.cs rename to src/Invex.Atom.Build/Model/ConsumedArtifact.cs index dfd1351d..9df95177 100644 --- a/DecSm.Atom/Build/Model/ConsumedArtifact.cs +++ b/src/Invex.Atom.Build/Model/ConsumedArtifact.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build.Model; +namespace Invex.Atom.Build.Model; /// /// Represents an artifact that is consumed by a target. diff --git a/DecSm.Atom/Build/Model/ConsumedVariable.cs b/src/Invex.Atom.Build/Model/ConsumedVariable.cs similarity index 91% rename from DecSm.Atom/Build/Model/ConsumedVariable.cs rename to src/Invex.Atom.Build/Model/ConsumedVariable.cs index f867f304..5ecb93f7 100644 --- a/DecSm.Atom/Build/Model/ConsumedVariable.cs +++ b/src/Invex.Atom.Build/Model/ConsumedVariable.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build.Model; +namespace Invex.Atom.Build.Model; /// /// Represents a variable that is consumed by a target. diff --git a/DecSm.Atom/Build/Model/ProducedArtifact.cs b/src/Invex.Atom.Build/Model/ProducedArtifact.cs similarity index 91% rename from DecSm.Atom/Build/Model/ProducedArtifact.cs rename to src/Invex.Atom.Build/Model/ProducedArtifact.cs index 5365ed07..8ab8ea3c 100644 --- a/DecSm.Atom/Build/Model/ProducedArtifact.cs +++ b/src/Invex.Atom.Build/Model/ProducedArtifact.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build.Model; +namespace Invex.Atom.Build.Model; /// /// Represents an artifact that is produced by a target. diff --git a/DecSm.Atom/Build/Model/TargetModel.cs b/src/Invex.Atom.Build/Model/TargetModel.cs similarity index 98% rename from DecSm.Atom/Build/Model/TargetModel.cs rename to src/Invex.Atom.Build/Model/TargetModel.cs index 79d1b42d..07a8e840 100644 --- a/DecSm.Atom/Build/Model/TargetModel.cs +++ b/src/Invex.Atom.Build/Model/TargetModel.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build.Model; +namespace Invex.Atom.Build.Model; /// /// Represents a target within the build model, defining its tasks, dependencies, inputs, and outputs. diff --git a/DecSm.Atom/Build/Model/TargetRunState.cs b/src/Invex.Atom.Build/Model/TargetRunState.cs similarity index 96% rename from DecSm.Atom/Build/Model/TargetRunState.cs rename to src/Invex.Atom.Build/Model/TargetRunState.cs index c950fdd2..f69021e4 100644 --- a/DecSm.Atom/Build/Model/TargetRunState.cs +++ b/src/Invex.Atom.Build/Model/TargetRunState.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build.Model; +namespace Invex.Atom.Build.Model; /// /// Represents the execution state of a target during a build run. diff --git a/DecSm.Atom/Build/Model/TargetState.cs b/src/Invex.Atom.Build/Model/TargetState.cs similarity index 95% rename from DecSm.Atom/Build/Model/TargetState.cs rename to src/Invex.Atom.Build/Model/TargetState.cs index 7929713f..fee27ad4 100644 --- a/DecSm.Atom/Build/Model/TargetState.cs +++ b/src/Invex.Atom.Build/Model/TargetState.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Build.Model; +namespace Invex.Atom.Build.Model; /// /// Represents the current state and execution metrics of a target during a build run. diff --git a/DecSm.Atom/Build/Model/UsedParam.cs b/src/Invex.Atom.Build/Model/UsedParam.cs similarity index 89% rename from DecSm.Atom/Build/Model/UsedParam.cs rename to src/Invex.Atom.Build/Model/UsedParam.cs index 8d2caccc..e1329109 100644 --- a/DecSm.Atom/Build/Model/UsedParam.cs +++ b/src/Invex.Atom.Build/Model/UsedParam.cs @@ -1,8 +1,9 @@ -namespace DecSm.Atom.Build.Model; +namespace Invex.Atom.Build.Model; /// /// Represents a parameter used by a build target, including its definition and whether it is required. /// /// The defining the parameter's metadata. /// A value indicating whether this parameter is required by the target. +[PublicAPI] public sealed record UsedParam(ParamModel Param, bool Required); diff --git a/DecSm.Atom/Params/ParamDefinition.cs b/src/Invex.Atom.Build/Params/ParamDefinition.cs similarity index 76% rename from DecSm.Atom/Params/ParamDefinition.cs rename to src/Invex.Atom.Build/Params/ParamDefinition.cs index ce077bee..3edb57ea 100644 --- a/DecSm.Atom/Params/ParamDefinition.cs +++ b/src/Invex.Atom.Build/Params/ParamDefinition.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Params; +namespace Invex.Atom.Build.Params; /// /// Represents the definition of a parameter used at runtime, including its name, source, and other metadata. @@ -12,6 +12,19 @@ public sealed record ParamDefinition(string Name) /// public required string ArgName { get; init; } + /// + /// Gets the environment variable name derived from . + /// The name is uppercased with hyphens, dots, and spaces replaced by underscores. + /// + [JsonIgnore] + public string EnvVarName => + ArgName + .Trim() + .ToUpperInvariant() + .Replace('-', '_') + .Replace('.', '_') + .Replace(' ', '_'); + /// /// Gets the description of the parameter, used for generating help text. /// diff --git a/DecSm.Atom/Params/ParamDefinitionAttribute.cs b/src/Invex.Atom.Build/Params/ParamDefinitionAttribute.cs similarity index 98% rename from DecSm.Atom/Params/ParamDefinitionAttribute.cs rename to src/Invex.Atom.Build/Params/ParamDefinitionAttribute.cs index 79d5a3b9..f3e865e0 100644 --- a/DecSm.Atom/Params/ParamDefinitionAttribute.cs +++ b/src/Invex.Atom.Build/Params/ParamDefinitionAttribute.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Params; +namespace Invex.Atom.Build.Params; // NOTE: If the constructor args are modified, the BuildDefinitionSourceGenerator will need to be updated diff --git a/DecSm.Atom/Params/ParamModel.cs b/src/Invex.Atom.Build/Params/ParamModel.cs similarity index 74% rename from DecSm.Atom/Params/ParamModel.cs rename to src/Invex.Atom.Build/Params/ParamModel.cs index 30b904dd..92d353f7 100644 --- a/DecSm.Atom/Params/ParamModel.cs +++ b/src/Invex.Atom.Build/Params/ParamModel.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Params; +namespace Invex.Atom.Build.Params; /// /// Represents the resolved metadata for a parameter, used for reflection and help generation. @@ -12,6 +12,19 @@ public sealed record ParamModel(string Name) /// public required string ArgName { get; init; } + /// + /// Gets the environment variable name derived from . + /// The name is uppercased with hyphens, dots, and spaces replaced by underscores. + /// + [JsonIgnore] + public string EnvVarName => + ArgName + .Trim() + .ToUpperInvariant() + .Replace('-', '_') + .Replace('.', '_') + .Replace(' ', '_'); + /// /// Gets the description of the parameter, used for help documentation. /// diff --git a/DecSm.Atom/Params/ParamService.cs b/src/Invex.Atom.Build/Params/ParamService.cs similarity index 97% rename from DecSm.Atom/Params/ParamService.cs rename to src/Invex.Atom.Build/Params/ParamService.cs index 8fbb6a1e..ea81a8d0 100644 --- a/DecSm.Atom/Params/ParamService.cs +++ b/src/Invex.Atom.Build/Params/ParamService.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Params; +namespace Invex.Atom.Build.Params; /// /// Defines a service for resolving and managing build parameters from various sources. @@ -129,7 +129,7 @@ IEnumerable secretsProviders /// private readonly AsyncLocal _defaultValuesOnly = new(); - private readonly List _knownSecrets = []; + private readonly ConcurrentBag _knownSecrets = []; /// /// Gets or sets a value indicating whether parameter caching is disabled. @@ -141,7 +141,9 @@ IEnumerable secretsProviders /// private readonly AsyncLocal _overrideSources = new(); - private readonly ISecretsProvider[] _secretsProviders = secretsProviders.ToArray(); + private readonly ISecretsProvider[] _secretsProviders = secretsProviders + .OrderByDescending(p => p.Priority) + .ToArray(); /// public IDisposable CreateNoCacheScope() => @@ -331,10 +333,7 @@ private static (bool HasValue, T? Value) TryGetParamFromEnvironmentVariables< envVar = Environment.GetEnvironmentVariable(paramDefinition.ArgName); if (string.IsNullOrEmpty(envVar)) - envVar = Environment.GetEnvironmentVariable(paramDefinition - .ArgName - .ToUpperInvariant() - .Replace('-', '_')); + envVar = Environment.GetEnvironmentVariable(paramDefinition.EnvVarName); if (string.IsNullOrEmpty(envVar)) return (false, default); @@ -462,16 +461,16 @@ public void Dispose() => private readonly record struct OverrideSourcesScope : IDisposable { private readonly ParamService _paramService; - private readonly ParamSource _sources; + private readonly ParamSource? _previousSources; public OverrideSourcesScope(ParamService paramService, ParamSource sources) { _paramService = paramService; - _sources = sources; + _previousSources = paramService._overrideSources.Value; paramService._overrideSources.Value = sources; } public void Dispose() => - _paramService._overrideSources.Value = _sources; + _paramService._overrideSources.Value = _previousSources; } } diff --git a/DecSm.Atom/Params/ParamSource.cs b/src/Invex.Atom.Build/Params/ParamSource.cs similarity index 95% rename from DecSm.Atom/Params/ParamSource.cs rename to src/Invex.Atom.Build/Params/ParamSource.cs index 1efdb1d4..263d4c0a 100644 --- a/DecSm.Atom/Params/ParamSource.cs +++ b/src/Invex.Atom.Build/Params/ParamSource.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Params; +namespace Invex.Atom.Build.Params; /// /// Specifies the sources from which a parameter's value can be resolved. @@ -35,7 +35,7 @@ public enum ParamSource /// /// The parameter can be resolved from a secret management system. /// - Secrets = 32, + Secrets = 16, /// /// The parameter can be resolved from any of the available sources. diff --git a/DecSm.Atom/Params/SecretDefinitionAttribute.cs b/src/Invex.Atom.Build/Params/SecretDefinitionAttribute.cs similarity index 97% rename from DecSm.Atom/Params/SecretDefinitionAttribute.cs rename to src/Invex.Atom.Build/Params/SecretDefinitionAttribute.cs index d786b73c..2b372533 100644 --- a/DecSm.Atom/Params/SecretDefinitionAttribute.cs +++ b/src/Invex.Atom.Build/Params/SecretDefinitionAttribute.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Params; +namespace Invex.Atom.Build.Params; /// /// Defines a secret parameter for a build target, which will be masked in logs and can be resolved from secret stores. diff --git a/DecSm.Atom/Reports/ConsoleOutcomeReportWriter.cs b/src/Invex.Atom.Build/Reports/ConsoleOutcomeReportWriter.cs similarity index 99% rename from DecSm.Atom/Reports/ConsoleOutcomeReportWriter.cs rename to src/Invex.Atom.Build/Reports/ConsoleOutcomeReportWriter.cs index fa7d4207..b9528611 100644 --- a/DecSm.Atom/Reports/ConsoleOutcomeReportWriter.cs +++ b/src/Invex.Atom.Build/Reports/ConsoleOutcomeReportWriter.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Reports; +namespace Invex.Atom.Build.Reports; /// /// An implementation of that writes a formatted build report to the console. diff --git a/DecSm.Atom/Reports/IOutcomeReportWriter.cs b/src/Invex.Atom.Build/Reports/IOutcomeReportWriter.cs similarity index 96% rename from DecSm.Atom/Reports/IOutcomeReportWriter.cs rename to src/Invex.Atom.Build/Reports/IOutcomeReportWriter.cs index b9a6a667..d10e5b15 100644 --- a/DecSm.Atom/Reports/IOutcomeReportWriter.cs +++ b/src/Invex.Atom.Build/Reports/IOutcomeReportWriter.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Reports; +namespace Invex.Atom.Build.Reports; /// /// Defines a contract for writing a build outcome report to a specific destination. diff --git a/DecSm.Atom/Reports/IReportsHelper.cs b/src/Invex.Atom.Build/Reports/IReportsHelper.cs similarity index 92% rename from DecSm.Atom/Reports/IReportsHelper.cs rename to src/Invex.Atom.Build/Reports/IReportsHelper.cs index 3cad8e4e..503652f7 100644 --- a/DecSm.Atom/Reports/IReportsHelper.cs +++ b/src/Invex.Atom.Build/Reports/IReportsHelper.cs @@ -1,8 +1,9 @@ -namespace DecSm.Atom.Reports; +namespace Invex.Atom.Build.Reports; /// /// Provides a helper for adding data to the build report. /// +[PublicAPI] public interface IReportsHelper : IBuildAccessor { /// diff --git a/DecSm.Atom/Reports/ReportData.cs b/src/Invex.Atom.Build/Reports/ReportData.cs similarity index 99% rename from DecSm.Atom/Reports/ReportData.cs rename to src/Invex.Atom.Build/Reports/ReportData.cs index e96e6e6d..cfbd4cbe 100644 --- a/DecSm.Atom/Reports/ReportData.cs +++ b/src/Invex.Atom.Build/Reports/ReportData.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Reports; +namespace Invex.Atom.Build.Reports; /// /// Defines the base interface for all data types that can be included in a build report. diff --git a/DecSm.Atom/Reports/ReportDataMarkdownFormatter.cs b/src/Invex.Atom.Build/Reports/ReportDataMarkdownFormatter.cs similarity index 99% rename from DecSm.Atom/Reports/ReportDataMarkdownFormatter.cs rename to src/Invex.Atom.Build/Reports/ReportDataMarkdownFormatter.cs index 675646df..d0ee2b9b 100644 --- a/DecSm.Atom/Reports/ReportDataMarkdownFormatter.cs +++ b/src/Invex.Atom.Build/Reports/ReportDataMarkdownFormatter.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Reports; +namespace Invex.Atom.Build.Reports; /// /// A static class that provides functionality to format a collection of objects diff --git a/DecSm.Atom/Reports/ReportService.cs b/src/Invex.Atom.Build/Reports/ReportService.cs similarity index 97% rename from DecSm.Atom/Reports/ReportService.cs rename to src/Invex.Atom.Build/Reports/ReportService.cs index d345f8ac..430c8e77 100644 --- a/DecSm.Atom/Reports/ReportService.cs +++ b/src/Invex.Atom.Build/Reports/ReportService.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Reports; +namespace Invex.Atom.Build.Reports; /// /// A service for collecting and managing data to be included in build reports. diff --git a/DecSm.Atom/Secrets/DotnetUserSecretsProvider.cs b/src/Invex.Atom.Build/Secrets/DotnetUserSecretsProvider.cs similarity index 94% rename from DecSm.Atom/Secrets/DotnetUserSecretsProvider.cs rename to src/Invex.Atom.Build/Secrets/DotnetUserSecretsProvider.cs index a09c27b6..b0d60298 100644 --- a/DecSm.Atom/Secrets/DotnetUserSecretsProvider.cs +++ b/src/Invex.Atom.Build/Secrets/DotnetUserSecretsProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Secrets; +namespace Invex.Atom.Build.Secrets; /// /// An implementation that retrieves secrets from the .NET user secrets store. @@ -17,6 +17,9 @@ internal sealed class DotnetUserSecretsProvider : ISecretsProvider /// public Assembly? SecretsAssembly { get; set; } + /// + public int Priority => 1; + /// /// Retrieves a secret value by its key from the .NET user secrets store. /// diff --git a/DecSm.Atom/Secrets/IDotnetUserSecrets.cs b/src/Invex.Atom.Build/Secrets/IDotnetUserSecrets.cs similarity index 91% rename from DecSm.Atom/Secrets/IDotnetUserSecrets.cs rename to src/Invex.Atom.Build/Secrets/IDotnetUserSecrets.cs index 7d1ec315..a08833f6 100644 --- a/DecSm.Atom/Secrets/IDotnetUserSecrets.cs +++ b/src/Invex.Atom.Build/Secrets/IDotnetUserSecrets.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Secrets; +namespace Invex.Atom.Build.Secrets; /// /// An interface that, when implemented on a build definition, enables sourcing secrets from the .NET user secrets @@ -26,13 +26,14 @@ /// /// /// +[PublicAPI] [ConfigureHostBuilder] public partial interface IDotnetUserSecrets { /// /// Configures the host builder to register the . /// - protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) => + protected static partial void ConfigureBuilderFromIDotnetUserSecrets(IHostApplicationBuilder builder) => builder .Services .AddSingleton() diff --git a/DecSm.Atom/Secrets/ISecretsProvider.cs b/src/Invex.Atom.Build/Secrets/ISecretsProvider.cs similarity index 85% rename from DecSm.Atom/Secrets/ISecretsProvider.cs rename to src/Invex.Atom.Build/Secrets/ISecretsProvider.cs index f8f4e219..fdd603d3 100644 --- a/DecSm.Atom/Secrets/ISecretsProvider.cs +++ b/src/Invex.Atom.Build/Secrets/ISecretsProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Secrets; +namespace Invex.Atom.Build.Secrets; /// /// Defines a contract for retrieving sensitive configuration values (secrets) from a secure storage provider. @@ -49,4 +49,13 @@ public interface ISecretsProvider /// allowing the framework to query other registered providers. /// string? GetSecret(string key); + + /// + /// Represents the execution or processing priority of the secrets provider. + /// + /// + /// A higher priority value indicates higher precedence when resolving secrets + /// among multiple providers. The default value is 0. + /// + int Priority => 0; } diff --git a/DecSm.Atom/Util/Scope/ActionScope.cs b/src/Invex.Atom.Build/Util/Scope/ActionScope.cs similarity index 93% rename from DecSm.Atom/Util/Scope/ActionScope.cs rename to src/Invex.Atom.Build/Util/Scope/ActionScope.cs index ad06b503..e2abd33e 100644 --- a/DecSm.Atom/Util/Scope/ActionScope.cs +++ b/src/Invex.Atom.Build/Util/Scope/ActionScope.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Util.Scope; +namespace Invex.Atom.Build.Util.Scope; /// /// A disposable scope that executes a provided action upon disposal. diff --git a/DecSm.Atom/Util/Scope/NullScope.cs b/src/Invex.Atom.Build/Util/Scope/NullScope.cs similarity index 83% rename from DecSm.Atom/Util/Scope/NullScope.cs rename to src/Invex.Atom.Build/Util/Scope/NullScope.cs index 3991fbdf..92759dd1 100644 --- a/DecSm.Atom/Util/Scope/NullScope.cs +++ b/src/Invex.Atom.Build/Util/Scope/NullScope.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Util.Scope; +namespace Invex.Atom.Build.Util.Scope; /// /// Represents a disposable scope that performs no action upon disposal. @@ -6,12 +6,13 @@ namespace DecSm.Atom.Util.Scope; /// /// This is useful in scenarios where a disposable object is required but no cleanup is necessary. /// +[PublicAPI] public readonly struct NullScope : IDisposable { /// /// Gets a singleton instance of the . /// - public static readonly NullScope Instance = new(); + public static readonly NullScope Instance; /// /// Performs no action. diff --git a/DecSm.Atom/Util/Scope/TaskScope.cs b/src/Invex.Atom.Build/Util/Scope/TaskScope.cs similarity index 94% rename from DecSm.Atom/Util/Scope/TaskScope.cs rename to src/Invex.Atom.Build/Util/Scope/TaskScope.cs index caaba223..7c61c047 100644 --- a/DecSm.Atom/Util/Scope/TaskScope.cs +++ b/src/Invex.Atom.Build/Util/Scope/TaskScope.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Util.Scope; +namespace Invex.Atom.Build.Util.Scope; /// /// A disposable scope that asynchronously executes a provided function upon disposal. diff --git a/DecSm.Atom/Util/StringUtil.cs b/src/Invex.Atom.Build/Util/StringUtil.cs similarity index 97% rename from DecSm.Atom/Util/StringUtil.cs rename to src/Invex.Atom.Build/Util/StringUtil.cs index b0b418a1..b8a08ea6 100644 --- a/DecSm.Atom/Util/StringUtil.cs +++ b/src/Invex.Atom.Build/Util/StringUtil.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Util; +namespace Invex.Atom.Build.Util; /// /// Provides utility methods for string manipulation and operations. @@ -82,7 +82,7 @@ public int GetLevenshteinDistance(string? compareTo) if (stripNewlines) @string = @string - .Replace(Environment.NewLine, " ") + .Replace("\r\n", " ") .Replace("\r", " ") .Replace("\n", " "); @@ -106,7 +106,7 @@ public int GetLevenshteinDistance(string? compareTo) /// unchanged. /// [return: NotNullIfNotNull(nameof(@string))] - public string? SanitizeSecrets(List secrets) + public string? SanitizeSecrets(IEnumerable secrets) { var validSecrets = secrets .Where(s => !string.IsNullOrEmpty(s)) diff --git a/DecSm.Atom/Util/TaskExtensions.cs b/src/Invex.Atom.Build/Util/TaskExtensions.cs similarity index 99% rename from DecSm.Atom/Util/TaskExtensions.cs rename to src/Invex.Atom.Build/Util/TaskExtensions.cs index b4213346..e720964a 100644 --- a/DecSm.Atom/Util/TaskExtensions.cs +++ b/src/Invex.Atom.Build/Util/TaskExtensions.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Util; +namespace Invex.Atom.Build.Util; /// /// Provides extension methods that add robust retry behavior to and diff --git a/DecSm.Atom/Util/TypeUtil.cs b/src/Invex.Atom.Build/Util/TypeUtil.cs similarity index 99% rename from DecSm.Atom/Util/TypeUtil.cs rename to src/Invex.Atom.Build/Util/TypeUtil.cs index 20d0f8d7..fde7201c 100644 --- a/DecSm.Atom/Util/TypeUtil.cs +++ b/src/Invex.Atom.Build/Util/TypeUtil.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Util; +namespace Invex.Atom.Build.Util; /// /// Provides flexible type conversion utilities, handling primitives, arrays, and generic collections. diff --git a/src/Invex.Atom.Build/Util/UnstableAPIAttribute.cs b/src/Invex.Atom.Build/Util/UnstableAPIAttribute.cs new file mode 100644 index 00000000..d28bc828 --- /dev/null +++ b/src/Invex.Atom.Build/Util/UnstableAPIAttribute.cs @@ -0,0 +1,7 @@ +namespace Invex.Atom.Build.Util; + +[UnstableAPI] +[AttributeUsage(AttributeTargets.All, Inherited = false)] +[MeansImplicitUse(ImplicitUseTargetFlags.WithMembers | ImplicitUseTargetFlags.WithInheritors)] +[SuppressMessage("ReSharper", "InconsistentNaming")] +public sealed class UnstableAPIAttribute : Attribute; diff --git a/DecSm.Atom/Variables/AtomWorkflowVariableProvider.cs b/src/Invex.Atom.Build/Variables/AtomVariableProvider.cs similarity index 91% rename from DecSm.Atom/Variables/AtomWorkflowVariableProvider.cs rename to src/Invex.Atom.Build/Variables/AtomVariableProvider.cs index cfd39ddd..ae1d65a0 100644 --- a/DecSm.Atom/Variables/AtomWorkflowVariableProvider.cs +++ b/src/Invex.Atom.Build/Variables/AtomVariableProvider.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Variables; +namespace Invex.Atom.Build.Variables; /// -/// The default implementation of , which uses a local JSON file for storage. +/// The default implementation of , which uses a local JSON file for storage. /// /// /// This provider serves as the fallback for variable management, persisting variables in a `variables` file @@ -9,8 +9,8 @@ /// /// The file system service for accessing the variables file. /// The build model for accessing the current target context. -internal sealed partial class AtomWorkflowVariableProvider(IAtomFileSystem fileSystem, BuildModel buildModel) - : IWorkflowVariableProvider +internal sealed partial class AtomVariableProvider(IRootedFileSystem fileSystem, BuildModel buildModel) + : IVariableProvider { /// /// Writes a variable to the local `variables` JSON file, scoped to the current job. diff --git a/DecSm.Atom/Variables/IWorkflowVariableProvider.cs b/src/Invex.Atom.Build/Variables/IVariableProvider.cs similarity index 91% rename from DecSm.Atom/Variables/IWorkflowVariableProvider.cs rename to src/Invex.Atom.Build/Variables/IVariableProvider.cs index 32c2acf4..c1c6f217 100644 --- a/DecSm.Atom/Variables/IWorkflowVariableProvider.cs +++ b/src/Invex.Atom.Build/Variables/IVariableProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Variables; +namespace Invex.Atom.Build.Variables; /// /// Defines a provider for reading and writing workflow variables to a specific storage backend. @@ -6,7 +6,7 @@ /// /// /// This interface is part of Atom's extensible variable management system. Multiple providers can be registered, -/// and the will delegate operations to them in a chain of responsibility. +/// and the will delegate operations to them in a chain of responsibility. /// /// /// Implementations should return true if they successfully handle an operation, or false to allow @@ -31,8 +31,9 @@ /// } /// /// -/// -public interface IWorkflowVariableProvider +/// +[PublicAPI] +public interface IVariableProvider { /// /// Writes a variable to the provider's storage system. diff --git a/DecSm.Atom/Variables/IVariablesHelper.cs b/src/Invex.Atom.Build/Variables/IVariablesHelper.cs similarity index 91% rename from DecSm.Atom/Variables/IVariablesHelper.cs rename to src/Invex.Atom.Build/Variables/IVariablesHelper.cs index 39d93fba..00053587 100644 --- a/DecSm.Atom/Variables/IVariablesHelper.cs +++ b/src/Invex.Atom.Build/Variables/IVariablesHelper.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Variables; +namespace Invex.Atom.Build.Variables; /// /// Provides helper methods for managing workflow variables within the Atom build system. @@ -15,7 +15,8 @@ /// /// /// -/// +/// +[PublicAPI] public interface IVariablesHelper : IBuildAccessor { /// @@ -30,6 +31,6 @@ public interface IVariablesHelper : IBuildAccessor /// If a variable with the same name already exists, it will be overwritten. /// Task WriteVariable(string name, string value, CancellationToken cancellationToken = default) => - GetService() + GetService() .WriteVariable(name, value, cancellationToken); } diff --git a/DecSm.Atom/Variables/WorkflowVariableService.cs b/src/Invex.Atom.Build/Variables/WorkflowVariableService.cs similarity index 85% rename from DecSm.Atom/Variables/WorkflowVariableService.cs rename to src/Invex.Atom.Build/Variables/WorkflowVariableService.cs index 96f6719a..518b9715 100644 --- a/DecSm.Atom/Variables/WorkflowVariableService.cs +++ b/src/Invex.Atom.Build/Variables/WorkflowVariableService.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Variables; +namespace Invex.Atom.Build.Variables; /// /// Defines a centralized service for managing workflow variables, coordinating operations across multiple providers. @@ -6,11 +6,11 @@ /// /// This service abstracts the complexity of multiple variable providers, offering a unified API for reading and /// writing -/// variables. It delegates operations to registered implementations, +/// variables. It delegates operations to registered implementations, /// trying custom providers first before falling back to the base Atom provider. /// [PublicAPI] -public interface IWorkflowVariableService +public interface IVariableService { /// /// Writes a variable to the workflow context, making it available for subsequent steps and jobs. @@ -40,29 +40,29 @@ public interface IWorkflowVariableService } /// -/// Internal implementation of . +/// Internal implementation of . /// /// The collection of registered workflow variable providers. /// The build definition for resolving parameter names. -internal sealed class WorkflowVariableService( - IEnumerable workflowVariableProviders, +internal sealed class VariableService( + IEnumerable workflowVariableProviders, IBuildDefinition buildDefinition -) : IWorkflowVariableService +) : IVariableService { /// /// The default provider, used as a fallback. /// // ReSharper disable once PossibleMultipleEnumeration - Once-only operation - private readonly AtomWorkflowVariableProvider _baseProvider = workflowVariableProviders - .OfType() + private readonly AtomVariableProvider _baseProvider = workflowVariableProviders + .OfType() .Single(); /// /// Custom providers, which are tried before the base provider. /// // ReSharper disable once PossibleMultipleEnumeration - Once-only operation - private readonly IWorkflowVariableProvider[] _customProviders = workflowVariableProviders - .Where(x => x is not AtomWorkflowVariableProvider) + private readonly IVariableProvider[] _customProviders = workflowVariableProviders + .Where(x => x is not AtomVariableProvider) .ToArray(); /// diff --git a/src/Invex.Atom.Build/_usings.cs b/src/Invex.Atom.Build/_usings.cs new file mode 100644 index 00000000..b7188eb9 --- /dev/null +++ b/src/Invex.Atom.Build/_usings.cs @@ -0,0 +1,41 @@ +global using System.Collections.Concurrent; +global using System.ComponentModel; +global using System.Diagnostics; +global using System.Diagnostics.CodeAnalysis; +global using System.Globalization; +global using System.Linq.Expressions; +global using System.Reflection; +global using System.Runtime.CompilerServices; +global using System.Text; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using System.Text.RegularExpressions; +global using Invex.Atom.Build.Args; +global using Invex.Atom.Build.BuildOptions; +global using Invex.Atom.Build.BuildInfo; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.Exceptions; +global using Invex.Atom.Build.FileSystem; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Build.Logging; +global using Invex.Atom.Build.Model; +global using Invex.Atom.Build.Params; +global using Invex.Atom.Build.Reports; +global using Invex.Atom.Build.Secrets; +global using Invex.Atom.Build.Util; +global using Invex.Atom.Build.Util.Scope; +global using Invex.Atom.Build.Variables; +global using JetBrains.Annotations; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Spectre.Console; +global using Spectre.Console.Rendering; +global using IHelpService = Invex.Atom.Build.Help.IHelpService; +global using HelpService = Invex.Atom.Build.Help.HelpService; + +[assembly: InternalsVisibleTo("Invex.Atom.Build.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +[assembly: SuppressMessage("ReSharper", "LocalizableElement")] diff --git a/DecSm.Atom.DotnetCliGenerator/Build.cs b/src/Invex.Atom.DotnetCliGenerator/Build.cs similarity index 82% rename from DecSm.Atom.DotnetCliGenerator/Build.cs rename to src/Invex.Atom.DotnetCliGenerator/Build.cs index b236a384..33a102b3 100644 --- a/DecSm.Atom.DotnetCliGenerator/Build.cs +++ b/src/Invex.Atom.DotnetCliGenerator/Build.cs @@ -7,11 +7,11 @@ /// It uses the `dotnet --cli-schema` command to obtain the latest .NET CLI command structure, /// then parses this schema and generates strongly-typed C# interfaces, implementations, and /// option classes for each `dotnet` command. The generated files are placed in the -/// `DecSm.Atom.Module.Dotnet/Cli/Generated` directory. +/// `Invex.Atom.Module.Dotnet/Cli/Generated` directory. /// [BuildDefinition] [GenerateEntryPoint] -internal sealed partial class Build : MinimalBuildDefinition +internal sealed partial class Build : BuildDefinition { /// /// The target that generates the .NET CLI wrapper code. @@ -41,10 +41,10 @@ internal sealed partial class Build : MinimalBuildDefinition /// /// /// - /// Writes these generated C# files to the `DecSm.Atom.Module.Dotnet/Cli/Generated` directory. + /// Writes these generated C# files to the `Invex.Atom.Module.Dotnet/Cli/Generated` directory. /// /// - /// This process ensures that the `DecSm.Atom.Module.Dotnet` always provides an up-to-date and type-safe API for the + /// This process ensures that the `Invex.Atom.Module.Dotnet` always provides an up-to-date and type-safe API for the /// .NET CLI. /// private Target GenerateDotnetCliCode => @@ -68,14 +68,14 @@ internal sealed partial class Build : MinimalBuildDefinition foreach (var subCommand in rootCommand.SubCommands) DotnetCliParser.FlattenCommands(string.Empty, subCommand, flattenedCommands); - if (!FileSystem.Directory.Exists(FileSystem.AtomRootDirectory / - "DecSm.Atom.Module.Dotnet" / - "Cli" / - "Generated")) - FileSystem.Directory.CreateDirectory(FileSystem.AtomRootDirectory / - "DecSm.Atom.Module.Dotnet" / - "Cli" / - "Generated"); + var generatedDirectory = FileSystem.AtomRootDirectory / + "src" / + "Invex.Atom.Module.Dotnet" / + "Cli" / + "Generated"; + + if (!FileSystem.Directory.Exists(generatedDirectory)) + FileSystem.Directory.CreateDirectory(generatedDirectory); // Filter commands // Also filter out internal/special commands @@ -93,7 +93,7 @@ internal sealed partial class Build : MinimalBuildDefinition writer.WriteLine(string.Empty); writer.WriteLine("#nullable enable"); writer.WriteLine(string.Empty); - writer.WriteLine("namespace DecSm.Atom.Module.Dotnet.Cli;"); + writer.WriteLine("namespace Invex.Atom.Module.Dotnet.Cli;"); writer.WriteLine(string.Empty); DotnetCliGenerator.GenerateInterfaceCode(writer, command); @@ -101,7 +101,8 @@ internal sealed partial class Build : MinimalBuildDefinition DotnetCliGenerator.GenerateOptionsCode(writer, command); await FileSystem.File.WriteAllTextAsync(FileSystem.AtomRootDirectory / - "DecSm.Atom.Module.Dotnet" / + "src" / + "Invex.Atom.Module.Dotnet" / "Cli" / "Generated" / $"{CsharpWriter.ToPascalCase(command.Name)}.cs", diff --git a/DecSm.Atom.DotnetCliGenerator/CsharpWriter.cs b/src/Invex.Atom.DotnetCliGenerator/CsharpWriter.cs similarity index 99% rename from DecSm.Atom.DotnetCliGenerator/CsharpWriter.cs rename to src/Invex.Atom.DotnetCliGenerator/CsharpWriter.cs index 3ab5bff1..3f73d92f 100644 --- a/DecSm.Atom.DotnetCliGenerator/CsharpWriter.cs +++ b/src/Invex.Atom.DotnetCliGenerator/CsharpWriter.cs @@ -245,7 +245,6 @@ public void WriteMethod( : $"{returnType} {name}("); using (Indent) - { for (var i = 0; i < validParameters.Count; i++) { var (paramType, paramName, defaultValue, _) = validParameters[i]; @@ -259,7 +258,6 @@ public void WriteMethod( : string.Empty, true); } - } WriteLine(");"); } @@ -278,7 +276,6 @@ public void WriteMethod( WriteLine($"{access} {returnType} {name}("); using (Indent) - { for (var i = 0; i < validParameters.Count; i++) { var (paramType, paramName, defaultValue, _) = validParameters[i]; @@ -292,7 +289,6 @@ public void WriteMethod( : string.Empty, true); } - } Write(")"); } diff --git a/DecSm.Atom.DotnetCliGenerator/IDotnetCliGenerator.cs b/src/Invex.Atom.DotnetCliGenerator/DotnetCliGenerator.cs similarity index 96% rename from DecSm.Atom.DotnetCliGenerator/IDotnetCliGenerator.cs rename to src/Invex.Atom.DotnetCliGenerator/DotnetCliGenerator.cs index 589ff6a6..e23c6518 100644 --- a/DecSm.Atom.DotnetCliGenerator/IDotnetCliGenerator.cs +++ b/src/Invex.Atom.DotnetCliGenerator/DotnetCliGenerator.cs @@ -4,7 +4,7 @@ /// Provides functionality to generate C# code for .NET CLI commands based on a parsed schema. /// /// -/// This static class is used by the definition in DecSm.Atom.DotnetCliGenerator +/// This static class is used by the definition in Invex.Atom.DotnetCliGenerator /// to create strongly-typed interfaces, implementations, and option classes for `dotnet` commands. /// public static class DotnetCliGenerator @@ -57,7 +57,7 @@ command with [ command.Arguments[0] with { - ValueType = "DecSm.Atom.Paths.RootedPath", + ValueType = "Invex.FileSystem.RootedPath", }, ], }, @@ -116,7 +116,7 @@ command with [ command.Arguments[0] with { - ValueType = "DecSm.Atom.Paths.RootedPath", + ValueType = "Invex.FileSystem.RootedPath", }, ], }, @@ -165,7 +165,6 @@ public static void GenerateOptionsCode(CsharpWriter writer, Command command) bodyWriter.WriteLine("return string.Join(' ',"); using (bodyWriter.Block("new[]")) - { foreach (var option in command.Options.Where(x => x.ValueType is not (null or "System.Void"))) { bodyWriter.WriteLine(option.ValueType is "System.Boolean" @@ -180,7 +179,6 @@ public static void GenerateOptionsCode(CsharpWriter writer, Command command) bodyWriter.WriteLine(": null,"); } } - } bodyWriter.WriteLine(".Where(x => x is { Length: > 0 }));"); }); @@ -203,8 +201,8 @@ private static void GenerateCommandMethod(CsharpWriter writer, Command command, .Arguments .Select(arg => new MethodParam(IsKnownAotFriendlyType(arg.ValueType) ? FormatType(arg.ValueType) - : arg.ValueType is "DecSm.Atom.Paths.RootedPath" - ? "DecSm.Atom.Paths.RootedPath" + : arg.ValueType is "Invex.FileSystem.RootedPath" + ? "Invex.FileSystem.RootedPath" : "System.String", CsharpWriter.ToPascalCase(arg.Name, true), XmlDescription: arg.Description)) @@ -228,7 +226,7 @@ private static void GenerateCommandMethod(CsharpWriter writer, Command command, { case { Arguments.Count: 0, Options.Count: 0 }: { - bodyWriter.WriteLine("return processRunner.RunAsync((processRunOptions is null"); + bodyWriter.WriteLine("processRunner.RunAsync((processRunOptions is null"); using (bodyWriter.Indent) { @@ -243,7 +241,7 @@ private static void GenerateCommandMethod(CsharpWriter writer, Command command, case { Arguments.Count: 0, Options.Count: > 0 }: { - bodyWriter.WriteLine("return processRunner.RunAsync((processRunOptions is null"); + bodyWriter.WriteLine("processRunner.RunAsync((processRunOptions is null"); using (bodyWriter.Indent) { @@ -377,7 +375,7 @@ private static string FormatToPrint(string type, string argName, string property /// true if the type is considered AOT-friendly; otherwise, false. /// /// This method uses simple string checks to classify types. It explicitly allows - /// `DecSm.Atom.Paths.RootedPath`, `Microsoft.DotNet.Cli.Utils.VerbosityOptions`, + /// `Invex.FileSystem.RootedPath`, `Microsoft.DotNet.Cli.Utils.VerbosityOptions`, /// and any type starting with "System." (assuming BCL types are generally AOT-friendly). /// private static bool IsKnownAotFriendlyType(string? typeName) @@ -397,7 +395,7 @@ private static bool IsKnownAotFriendlyType(string? typeName) typeName = typeName[(typeName.IndexOf('`') + 1)..]; // Explicitly allow our known custom types and consider all BCL types under System.* as known - return typeName is "DecSm.Atom.Paths.RootedPath" or "Microsoft.DotNet.Cli.Utils.VerbosityOptions" || + return typeName is "Invex.FileSystem.RootedPath" or "Microsoft.DotNet.Cli.Utils.VerbosityOptions" || typeName.StartsWith("System.", StringComparison.Ordinal); } } diff --git a/DecSm.Atom.DotnetCliGenerator/DotnetCliParser.cs b/src/Invex.Atom.DotnetCliGenerator/DotnetCliParser.cs similarity index 100% rename from DecSm.Atom.DotnetCliGenerator/DotnetCliParser.cs rename to src/Invex.Atom.DotnetCliGenerator/DotnetCliParser.cs diff --git a/DecSm.Atom.DotnetCliGenerator/DecSm.Atom.DotnetCliGenerator.csproj b/src/Invex.Atom.DotnetCliGenerator/Invex.Atom.DotnetCliGenerator.csproj similarity index 68% rename from DecSm.Atom.DotnetCliGenerator/DecSm.Atom.DotnetCliGenerator.csproj rename to src/Invex.Atom.DotnetCliGenerator/Invex.Atom.DotnetCliGenerator.csproj index 68a4589a..22cdc6af 100644 --- a/DecSm.Atom.DotnetCliGenerator/DecSm.Atom.DotnetCliGenerator.csproj +++ b/src/Invex.Atom.DotnetCliGenerator/Invex.Atom.DotnetCliGenerator.csproj @@ -21,13 +21,9 @@ - - - + + + diff --git a/DecSm.Atom.DotnetCliGenerator/Properties/launchSettings.json b/src/Invex.Atom.DotnetCliGenerator/Properties/launchSettings.json similarity index 100% rename from DecSm.Atom.DotnetCliGenerator/Properties/launchSettings.json rename to src/Invex.Atom.DotnetCliGenerator/Properties/launchSettings.json diff --git a/src/Invex.Atom.DotnetCliGenerator/_usings.cs b/src/Invex.Atom.DotnetCliGenerator/_usings.cs new file mode 100644 index 00000000..e43dbf51 --- /dev/null +++ b/src/Invex.Atom.DotnetCliGenerator/_usings.cs @@ -0,0 +1,8 @@ +global using System.Text; +global using System.Text.Json; +global using System.Text.RegularExpressions; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.FileSystem; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Build.Util.Scope; +global using JetBrains.Annotations; diff --git a/src/Invex.Atom.Module.AzureKeyVault/AzureKeyOptionsProvider.cs b/src/Invex.Atom.Module.AzureKeyVault/AzureKeyOptionsProvider.cs new file mode 100644 index 00000000..15f0be38 --- /dev/null +++ b/src/Invex.Atom.Module.AzureKeyVault/AzureKeyOptionsProvider.cs @@ -0,0 +1,111 @@ +namespace Invex.Atom.Module.AzureKeyVault; + +/// +/// Provides an implementation of . +/// +/// +/// This provider integrates with build options to define how Azure Key Vault parameters are injected. +/// +[PublicAPI] +public sealed class AzureKeyOptionsProvider(IAzureKeyVault keyVault) : IBuildOptionProvider +{ + /// + /// Gets additional build options based on the Azure Key Vault injection configuration. + /// + /// + /// This method dynamically generates build options based on the + /// + /// configuration, allowing for flexible secret injection strategies. + /// + public IReadOnlyList GetBuildOptions(IReadOnlyList baseOptions) + { + if (!UseAzureKeyVault.IsEnabled(baseOptions)) + return []; + + var injections = keyVault.AzureKeyVaultValueInjections; + + var valueInjections = new List(); + + switch (injections.Address) + { + case AzureKeyVaultValueInjectionType.EnvironmentVariable: + valueInjections.Add( + BuildOptions.Inject.SecretFromWorkflowEnvironment(nameof(IAzureKeyVault.AzureVaultAddress))); + + break; + case AzureKeyVaultValueInjectionType.Secret: + valueInjections.Add( + BuildOptions.Inject.SecretForSecretProvider(nameof(IAzureKeyVault.AzureVaultAddress))); + + break; + case AzureKeyVaultValueInjectionType.None: + + break; + default: + + throw new ArgumentOutOfRangeException(nameof(injections.Address), injections.Address, null); + } + + switch (injections.TenantId) + { + case AzureKeyVaultValueInjectionType.EnvironmentVariable: + valueInjections.Add( + BuildOptions.Inject.SecretFromWorkflowEnvironment(nameof(IAzureKeyVault.AzureVaultTenantId))); + + break; + case AzureKeyVaultValueInjectionType.Secret: + valueInjections.Add( + BuildOptions.Inject.SecretForSecretProvider(nameof(IAzureKeyVault.AzureVaultTenantId))); + + break; + case AzureKeyVaultValueInjectionType.None: + + break; + default: + + throw new ArgumentOutOfRangeException(nameof(injections.TenantId), injections.TenantId, null); + } + + switch (injections.AppId) + { + case AzureKeyVaultValueInjectionType.EnvironmentVariable: + valueInjections.Add( + BuildOptions.Inject.SecretFromWorkflowEnvironment(nameof(IAzureKeyVault.AzureVaultAppId))); + + break; + case AzureKeyVaultValueInjectionType.Secret: + valueInjections.Add( + BuildOptions.Inject.SecretForSecretProvider(nameof(IAzureKeyVault.AzureVaultAppId))); + + break; + case AzureKeyVaultValueInjectionType.None: + + break; + default: + + throw new ArgumentOutOfRangeException(nameof(injections.AppId), injections.AppId, null); + } + + switch (injections.AppSecret) + { + case AzureKeyVaultValueInjectionType.EnvironmentVariable: + valueInjections.Add( + BuildOptions.Inject.SecretFromWorkflowEnvironment(nameof(IAzureKeyVault.AzureVaultAppSecret))); + + break; + case AzureKeyVaultValueInjectionType.Secret: + valueInjections.Add( + BuildOptions.Inject.SecretForSecretProvider(nameof(IAzureKeyVault.AzureVaultAppSecret))); + + break; + case AzureKeyVaultValueInjectionType.None: + + break; + default: + + throw new ArgumentOutOfRangeException(nameof(injections.AppSecret), injections.AppSecret, null); + } + + return valueInjections; + } +} diff --git a/DecSm.Atom.Module.AzureKeyVault/AzureKeySecretsProvider.cs b/src/Invex.Atom.Module.AzureKeyVault/AzureKeySecretsProvider.cs similarity index 58% rename from DecSm.Atom.Module.AzureKeyVault/AzureKeySecretsProvider.cs rename to src/Invex.Atom.Module.AzureKeyVault/AzureKeySecretsProvider.cs index dba62402..b093f479 100644 --- a/DecSm.Atom.Module.AzureKeyVault/AzureKeySecretsProvider.cs +++ b/src/Invex.Atom.Module.AzureKeyVault/AzureKeySecretsProvider.cs @@ -1,13 +1,11 @@ -namespace DecSm.Atom.Module.AzureKeyVault; +namespace Invex.Atom.Module.AzureKeyVault; /// -/// Provides an implementation of and -/// for retrieving secrets from Azure Key Vault. +/// Provides an implementation of for retrieving secrets from Azure Key Vault. /// /// /// This provider connects to Azure Key Vault using the parameters defined in -/// and allows for dynamic retrieval of secrets during the build process. It also integrates with -/// workflow options to define how Azure Key Vault parameters are injected. +/// and allows for dynamic retrieval of secrets during the build process. /// [PublicAPI] public sealed class AzureKeySecretsProvider( @@ -15,7 +13,7 @@ public sealed class AzureKeySecretsProvider( CommandLineArgs args, IAzureKeyVault keyVault, ILogger logger -) : ISecretsProvider, IWorkflowOptionProvider +) : ISecretsProvider { private SecretClient? _secretClient; @@ -59,109 +57,6 @@ or nameof(IAzureKeyVault.AzureVaultAppId)) } } - /// - /// Gets a read-only list of workflow options related to Azure Key Vault parameter injection. - /// - /// - /// This property dynamically generates workflow options based on the - /// - /// configuration, allowing for flexible secret injection strategies. - /// - public IReadOnlyList WorkflowOptions - { - get - { - if (!UseAzureKeyVault.IsEnabled( - buildDefinition.GlobalWorkflowOptions.Concat(buildDefinition.Workflows.SelectMany(x => x.Options)))) - return []; - - var injections = keyVault.AzureKeyVaultValueInjections; - - var valueInjections = new List(); - - switch (injections.Address) - { - case AzureKeyVaultValueInjectionType.EnvironmentVariable: - valueInjections.Add( - WorkflowSecretsEnvironmentInjection.Create(nameof(IAzureKeyVault.AzureVaultAddress))); - - break; - case AzureKeyVaultValueInjectionType.Secret: - valueInjections.Add( - WorkflowSecretsSecretInjection.Create(nameof(IAzureKeyVault.AzureVaultAddress))); - - break; - case AzureKeyVaultValueInjectionType.None: - - break; - default: - - throw new ArgumentOutOfRangeException(nameof(injections.Address), injections.Address, null); - } - - switch (injections.TenantId) - { - case AzureKeyVaultValueInjectionType.EnvironmentVariable: - valueInjections.Add( - WorkflowSecretsEnvironmentInjection.Create(nameof(IAzureKeyVault.AzureVaultTenantId))); - - break; - case AzureKeyVaultValueInjectionType.Secret: - valueInjections.Add( - WorkflowSecretsSecretInjection.Create(nameof(IAzureKeyVault.AzureVaultTenantId))); - - break; - case AzureKeyVaultValueInjectionType.None: - - break; - default: - - throw new ArgumentOutOfRangeException(nameof(injections.TenantId), injections.TenantId, null); - } - - switch (injections.AppId) - { - case AzureKeyVaultValueInjectionType.EnvironmentVariable: - valueInjections.Add( - WorkflowSecretsEnvironmentInjection.Create(nameof(IAzureKeyVault.AzureVaultAppId))); - - break; - case AzureKeyVaultValueInjectionType.Secret: - valueInjections.Add(WorkflowSecretsSecretInjection.Create(nameof(IAzureKeyVault.AzureVaultAppId))); - - break; - case AzureKeyVaultValueInjectionType.None: - - break; - default: - - throw new ArgumentOutOfRangeException(nameof(injections.AppId), injections.AppId, null); - } - - switch (injections.AppSecret) - { - case AzureKeyVaultValueInjectionType.EnvironmentVariable: - valueInjections.Add( - WorkflowSecretsEnvironmentInjection.Create(nameof(IAzureKeyVault.AzureVaultAppSecret))); - - break; - case AzureKeyVaultValueInjectionType.Secret: - valueInjections.Add( - WorkflowSecretsSecretInjection.Create(nameof(IAzureKeyVault.AzureVaultAppSecret))); - - break; - case AzureKeyVaultValueInjectionType.None: - - break; - default: - - throw new ArgumentOutOfRangeException(nameof(injections.AppSecret), injections.AppSecret, null); - } - - return valueInjections; - } - } - /// /// Obtains the appropriate for authenticating with Azure Key Vault. /// diff --git a/DecSm.Atom.Module.AzureKeyVault/IAzureKeyVault.cs b/src/Invex.Atom.Module.AzureKeyVault/IAzureKeyVault.cs similarity index 93% rename from DecSm.Atom.Module.AzureKeyVault/IAzureKeyVault.cs rename to src/Invex.Atom.Module.AzureKeyVault/IAzureKeyVault.cs index aab8aa38..7a84dce1 100644 --- a/DecSm.Atom.Module.AzureKeyVault/IAzureKeyVault.cs +++ b/src/Invex.Atom.Module.AzureKeyVault/IAzureKeyVault.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.AzureKeyVault; +namespace Invex.Atom.Module.AzureKeyVault; /// /// Provides integration with Azure Key Vault for managing secrets and configuration. @@ -7,6 +7,7 @@ /// This interface defines the parameters required to connect to an Azure Key Vault /// and configures the necessary services for retrieving secrets. /// +[PublicAPI] [ConfigureHostBuilder] public partial interface IAzureKeyVault : IBuildAccessor { @@ -78,12 +79,11 @@ public partial interface IAzureKeyVault : IBuildAccessor /// This method registers as a singleton service, /// making it available for secret retrieval and workflow option provisioning throughout the build. /// - protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) => + protected static partial void ConfigureBuilderFromIAzureKeyVault(IHostApplicationBuilder builder) => builder .Services - .AddSingleton() - .AddSingleton(x => x.GetRequiredService()) - .AddSingleton(x => x.GetRequiredService()); + .AddSingleton() + .AddSingleton(); } /// @@ -93,6 +93,7 @@ protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) /// The injection type for the Azure Vault Tenant ID. /// The injection type for the Azure Vault Application ID. /// The injection type for the Azure Vault Application Secret. +[PublicAPI] public sealed record AzureKeyVaultValueInjections( AzureKeyVaultValueInjectionType Address = AzureKeyVaultValueInjectionType.EnvironmentVariable, AzureKeyVaultValueInjectionType TenantId = AzureKeyVaultValueInjectionType.EnvironmentVariable, diff --git a/DecSm.Atom.Module.AzureKeyVault/DecSm.Atom.Module.AzureKeyVault.csproj b/src/Invex.Atom.Module.AzureKeyVault/Invex.Atom.Module.AzureKeyVault.csproj similarity index 73% rename from DecSm.Atom.Module.AzureKeyVault/DecSm.Atom.Module.AzureKeyVault.csproj rename to src/Invex.Atom.Module.AzureKeyVault/Invex.Atom.Module.AzureKeyVault.csproj index 8e26d3ba..955a8ec4 100644 --- a/DecSm.Atom.Module.AzureKeyVault/DecSm.Atom.Module.AzureKeyVault.csproj +++ b/src/Invex.Atom.Module.AzureKeyVault/Invex.Atom.Module.AzureKeyVault.csproj @@ -10,6 +10,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -25,12 +26,12 @@ - - + + - + \ No newline at end of file diff --git a/src/Invex.Atom.Module.AzureKeyVault/Invex.Atom.Module.AzureKeyVault.props b/src/Invex.Atom.Module.AzureKeyVault/Invex.Atom.Module.AzureKeyVault.props new file mode 100644 index 00000000..dec7ad41 --- /dev/null +++ b/src/Invex.Atom.Module.AzureKeyVault/Invex.Atom.Module.AzureKeyVault.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Invex.Atom.Module.AzureKeyVault/Options/AzureKeyVaultBuildOptionsExtensions.cs b/src/Invex.Atom.Module.AzureKeyVault/Options/AzureKeyVaultBuildOptionsExtensions.cs new file mode 100644 index 00000000..8e701de1 --- /dev/null +++ b/src/Invex.Atom.Module.AzureKeyVault/Options/AzureKeyVaultBuildOptionsExtensions.cs @@ -0,0 +1,25 @@ +namespace Invex.Atom.Module.AzureKeyVault.Options; + +[PublicAPI] +[SuppressMessage("Performance", "CA1822:Mark members as static")] +public static class AzureKeyVaultBuildOptionsExtensions +{ + [PublicAPI] + public sealed class AzureKeyVaultBuildOptions + { + public static AzureKeyVaultBuildOptions Instance => field ??= new(); + + public UseAzureKeyVault UseAzureKeyVault => field ??= new(); + + public UseAzureKeyVault SetUseAzureKeyVault(bool value) => + new() + { + Enabled = value, + }; + } + + extension(BuildOptions) + { + public static AzureKeyVaultBuildOptions AzureKeyVault => AzureKeyVaultBuildOptions.Instance; + } +} diff --git a/src/Invex.Atom.Module.AzureKeyVault/Options/UseAzureKeyVault.cs b/src/Invex.Atom.Module.AzureKeyVault/Options/UseAzureKeyVault.cs new file mode 100644 index 00000000..25d0fa3a --- /dev/null +++ b/src/Invex.Atom.Module.AzureKeyVault/Options/UseAzureKeyVault.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Module.AzureKeyVault.Options; + +[PublicAPI] +public sealed record UseAzureKeyVault : ToggleBuildOption; diff --git a/src/Invex.Atom.Module.AzureKeyVault/_usings.cs b/src/Invex.Atom.Module.AzureKeyVault/_usings.cs new file mode 100644 index 00000000..4b1527a1 --- /dev/null +++ b/src/Invex.Atom.Module.AzureKeyVault/_usings.cs @@ -0,0 +1,17 @@ +global using System.Diagnostics.CodeAnalysis; +global using Azure.Core; +global using Azure.Identity; +global using Azure.Security.KeyVault.Secrets; +global using Invex.Atom.Build.Args; +global using Invex.Atom.Build; +global using Invex.Atom.Build.BuildOptions; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Build.Params; +global using Invex.Atom.Build.Secrets; +global using Invex.Atom.Module.AzureKeyVault.Options; +global using Invex.Atom.Workflows.Options; +global using JetBrains.Annotations; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; diff --git a/DecSm.Atom.Module.AzureStorage/AzureBlobArtifactProvider.cs b/src/Invex.Atom.Module.AzureStorage/AzureBlobArtifactProvider.cs similarity index 97% rename from DecSm.Atom.Module.AzureStorage/AzureBlobArtifactProvider.cs rename to src/Invex.Atom.Module.AzureStorage/AzureBlobArtifactProvider.cs index 14f5686f..46eb4ec3 100644 --- a/DecSm.Atom.Module.AzureStorage/AzureBlobArtifactProvider.cs +++ b/src/Invex.Atom.Module.AzureStorage/AzureBlobArtifactProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.AzureStorage; +namespace Invex.Atom.Module.AzureStorage; /// /// Provides an implementation of for storing and retrieving @@ -6,14 +6,14 @@ /// /// /// This provider uses the Azure Storage SDK to interact with blob containers, -/// allowing for robust artifact management within DecSm.Atom build processes. +/// allowing for robust artifact management within Invex.Atom build processes. /// [PublicAPI] public sealed class AzureBlobArtifactProvider( IBuildInfo buildInfo, IParamService paramService, ReportService reportService, - IAtomFileSystem fileSystem, + IRootedFileSystem fileSystem, IBuildIdProvider buildIdProvider, ILogger logger ) : IArtifactProvider @@ -48,7 +48,7 @@ ILogger logger /// Thrown if a build ID is required but not available, or if no files are found in an artifact directory. /// public async Task StoreArtifacts( - IReadOnlyList artifactNames, + IEnumerable artifactNames, string? buildId = null, string? slice = null, CancellationToken cancellationToken = default) @@ -148,7 +148,7 @@ await blobClient.UploadAsync(file, /// Thrown if a build ID is required but not available, or if no blobs are found for the specified artifact. /// public async Task RetrieveArtifacts( - IReadOnlyList artifactNames, + IEnumerable artifactNames, string? buildId = null, string? buildSlice = null, CancellationToken cancellationToken = default) @@ -254,7 +254,7 @@ public async Task RetrieveArtifacts( /// A list of build IDs for which to clean up artifacts. /// A cancellation token to observe while waiting for the task to complete. /// A representing the asynchronous operation. - public async Task Cleanup(IReadOnlyList runIdentifiers, CancellationToken cancellationToken = default) + public async Task Cleanup(IEnumerable runIdentifiers, CancellationToken cancellationToken = default) { var connectionString = paramService.GetParam(nameof(IAzureArtifactStorage.AzureArtifactStorageConnectionString)); diff --git a/DecSm.Atom.Module.AzureStorage/IAzureArtifactStorage.cs b/src/Invex.Atom.Module.AzureStorage/IAzureArtifactStorage.cs similarity index 92% rename from DecSm.Atom.Module.AzureStorage/IAzureArtifactStorage.cs rename to src/Invex.Atom.Module.AzureStorage/IAzureArtifactStorage.cs index bdc1a8f9..728dbf2f 100644 --- a/DecSm.Atom.Module.AzureStorage/IAzureArtifactStorage.cs +++ b/src/Invex.Atom.Module.AzureStorage/IAzureArtifactStorage.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.AzureStorage; +namespace Invex.Atom.Module.AzureStorage; /// /// Provides an interface for configuring Azure Blob Storage as an artifact store. @@ -7,6 +7,7 @@ /// This interface extends and , /// enabling the build to both upload and download artifacts from Azure Blob Storage. /// +[PublicAPI] [ConfigureHostBuilder] public partial interface IAzureArtifactStorage : IStoreArtifact, IRetrieveArtifact { @@ -40,6 +41,6 @@ public partial interface IAzureArtifactStorage : IStoreArtifact, IRetrieveArtifa /// implementation for , making Azure Blob Storage /// available for artifact management throughout the build. /// - protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) => + protected static partial void ConfigureBuilderFromIAzureArtifactStorage(IHostApplicationBuilder builder) => builder.Services.AddSingleton(); } diff --git a/DecSm.Atom.Module.AzureStorage/DecSm.Atom.Module.AzureStorage.csproj b/src/Invex.Atom.Module.AzureStorage/Invex.Atom.Module.AzureStorage.csproj similarity index 76% rename from DecSm.Atom.Module.AzureStorage/DecSm.Atom.Module.AzureStorage.csproj rename to src/Invex.Atom.Module.AzureStorage/Invex.Atom.Module.AzureStorage.csproj index e4587936..cfe3f384 100644 --- a/DecSm.Atom.Module.AzureStorage/DecSm.Atom.Module.AzureStorage.csproj +++ b/src/Invex.Atom.Module.AzureStorage/Invex.Atom.Module.AzureStorage.csproj @@ -6,14 +6,12 @@ - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -29,12 +27,12 @@ - - + + - + \ No newline at end of file diff --git a/src/Invex.Atom.Module.AzureStorage/Invex.Atom.Module.AzureStorage.props b/src/Invex.Atom.Module.AzureStorage/Invex.Atom.Module.AzureStorage.props new file mode 100644 index 00000000..db680a7f --- /dev/null +++ b/src/Invex.Atom.Module.AzureStorage/Invex.Atom.Module.AzureStorage.props @@ -0,0 +1,5 @@ + + + + + diff --git a/DecSm.Atom.Module.AzureStorage/_usings.cs b/src/Invex.Atom.Module.AzureStorage/_usings.cs similarity index 51% rename from DecSm.Atom.Module.AzureStorage/_usings.cs rename to src/Invex.Atom.Module.AzureStorage/_usings.cs index 82dc00cf..597c86c6 100644 --- a/DecSm.Atom.Module.AzureStorage/_usings.cs +++ b/src/Invex.Atom.Module.AzureStorage/_usings.cs @@ -1,13 +1,13 @@ global using System.Text.RegularExpressions; global using Azure.Storage.Blobs; global using Azure.Storage.Blobs.Models; -global using DecSm.Atom.Artifacts; -global using DecSm.Atom.BuildInfo; -global using DecSm.Atom.Hosting; -global using DecSm.Atom.Params; -global using DecSm.Atom.Paths; -global using DecSm.Atom.Reports; -global using DecSm.Atom.Util; +global using Invex.Atom.Build.Artifacts; +global using Invex.Atom.Build.BuildInfo; +global using Invex.Atom.Build.FileSystem; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Build.Params; +global using Invex.Atom.Build.Reports; +global using Invex.Atom.Build.Util; global using JetBrains.Annotations; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Hosting; diff --git a/DecSm.Atom.Module.DevopsWorkflows/Devops.cs b/src/Invex.Atom.Module.DevopsWorkflows/Devops.cs similarity index 99% rename from DecSm.Atom.Module.DevopsWorkflows/Devops.cs rename to src/Invex.Atom.Module.DevopsWorkflows/Devops.cs index e5b403da..9af6fe05 100644 --- a/DecSm.Atom.Module.DevopsWorkflows/Devops.cs +++ b/src/Invex.Atom.Module.DevopsWorkflows/Devops.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.DevopsWorkflows; +namespace Invex.Atom.Module.DevopsWorkflows; /// /// Provides utility methods and properties for interacting with Azure DevOps Pipelines. diff --git a/DecSm.Atom.Module.DevopsWorkflows/DevopsSummaryOutcomeReportWriter.cs b/src/Invex.Atom.Module.DevopsWorkflows/DevopsSummaryOutcomeReportWriter.cs similarity index 95% rename from DecSm.Atom.Module.DevopsWorkflows/DevopsSummaryOutcomeReportWriter.cs rename to src/Invex.Atom.Module.DevopsWorkflows/DevopsSummaryOutcomeReportWriter.cs index c08ea509..11113748 100644 --- a/DecSm.Atom.Module.DevopsWorkflows/DevopsSummaryOutcomeReportWriter.cs +++ b/src/Invex.Atom.Module.DevopsWorkflows/DevopsSummaryOutcomeReportWriter.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.DevopsWorkflows; +namespace Invex.Atom.Module.DevopsWorkflows; /// /// Implements to write build outcome reports to Azure DevOps build summary. @@ -9,7 +9,7 @@ /// It also masks any secrets present in the report text. /// internal sealed class DevopsSummaryOutcomeReportWriter( - IAtomFileSystem fileSystem, + IRootedFileSystem fileSystem, ReportService reportService, IParamService paramService ) : IOutcomeReportWriter diff --git a/DecSm.Atom.Module.DevopsWorkflows/DevopsVariableProvider.cs b/src/Invex.Atom.Module.DevopsWorkflows/DevopsVariableProvider.cs similarity index 93% rename from DecSm.Atom.Module.DevopsWorkflows/DevopsVariableProvider.cs rename to src/Invex.Atom.Module.DevopsWorkflows/DevopsVariableProvider.cs index 05f49cf6..18e83ed3 100644 --- a/DecSm.Atom.Module.DevopsWorkflows/DevopsVariableProvider.cs +++ b/src/Invex.Atom.Module.DevopsWorkflows/DevopsVariableProvider.cs @@ -1,13 +1,13 @@ -namespace DecSm.Atom.Module.DevopsWorkflows; +namespace Invex.Atom.Module.DevopsWorkflows; /// -/// Provides an implementation of for Azure DevOps Pipelines. +/// Provides an implementation of for Azure DevOps Pipelines. /// /// /// This provider enables writing output variables that can be consumed by subsequent steps or jobs /// within an Azure DevOps Pipeline. It also supports reading variables from previous jobs. /// -internal sealed class DevopsVariableProvider(ILogger logger) : IWorkflowVariableProvider +internal sealed class DevopsVariableProvider(ILogger logger) : IVariableProvider { /// /// Writes a variable to the Azure DevOps Pipeline output, making it available to subsequent steps or jobs. diff --git a/src/Invex.Atom.Module.DevopsWorkflows/DevopsWorkflowContextProvider.cs b/src/Invex.Atom.Module.DevopsWorkflows/DevopsWorkflowContextProvider.cs new file mode 100644 index 00000000..ead96df8 --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/DevopsWorkflowContextProvider.cs @@ -0,0 +1,14 @@ +namespace Invex.Atom.Module.DevopsWorkflows; + +internal sealed class DevopsWorkflowContextProvider : IWorkflowContextProvider +{ + public IWorkflowType? WorkflowType => + Devops.IsDevopsPipelines + ? Devops.WorkflowType + : null; + + public string? WorkflowName => + Devops.IsDevopsPipelines + ? Devops.Variables.BuildDefinitionName + : null; +} diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Extensions/DevopsBuildOptionsExtensions.cs b/src/Invex.Atom.Module.DevopsWorkflows/Extensions/DevopsBuildOptionsExtensions.cs new file mode 100644 index 00000000..e8653432 --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/Extensions/DevopsBuildOptionsExtensions.cs @@ -0,0 +1,179 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Extensions; + +[PublicAPI] +[SuppressMessage("Performance", "CA1822:Mark members as static")] +public static class DevopsBuildOptionsExtensions +{ + [PublicAPI] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public sealed class DevopsPoolOptions + { + // Windows + public DevopsPool Windows_2025_Vs2026 => + field ??= new() + { + HostedImage = WorkflowLabels.Devops.Pool.Windows_2025_Vs2026, + }; + + public DevopsPool Windows_Latest => + field ??= new() + { + HostedImage = WorkflowLabels.Devops.Pool.Windows_Latest, + }; + + public DevopsPool Windows_2025 => + field ??= new() + { + HostedImage = WorkflowLabels.Devops.Pool.Windows_2025, + }; + + public DevopsPool Windows_2022 => + field ??= new() + { + HostedImage = WorkflowLabels.Devops.Pool.Windows_2022, + }; + + // Linux + public DevopsPool Ubuntu_Latest => + field ??= new() + { + HostedImage = WorkflowLabels.Devops.Pool.Ubuntu_Latest, + }; + + public DevopsPool Ubuntu_24_04 => + field ??= new() + { + HostedImage = WorkflowLabels.Devops.Pool.Ubuntu_24_04, + }; + + public DevopsPool Ubuntu_22_04 => + field ??= new() + { + HostedImage = WorkflowLabels.Devops.Pool.Ubuntu_22_04, + }; + + // MacOS + public DevopsPool MacOs_Latest => + field ??= new() + { + HostedImage = WorkflowLabels.Devops.Pool.MacOs_Latest, + }; + + public DevopsPool MacOs_15 => + field ??= new() + { + HostedImage = WorkflowLabels.Devops.Pool.MacOs_15, + }; + + public DevopsPool MacOs_14 => + field ??= new() + { + HostedImage = WorkflowLabels.Devops.Pool.MacOs_14, + }; + + public DevopsPool SetByMatrix => + field ??= new() + { + HostedImage = "$(job-runs-on)", + }; + + public DevopsPool FromName(TextExpression name) => + new() + { + HostedImage = name, + }; + + public DevopsPool FromName(string name) => + new() + { + HostedImage = name, + }; + + public DevopsPool FromHostedImage(TextExpression hostedImageName) => + new() + { + HostedImage = hostedImageName, + }; + + public DevopsPool FromHostedImage(string hostedImageName) => + new() + { + HostedImage = hostedImageName, + }; + + public DevopsPool FromDemands(params TextExpression[] demands) => + new() + { + Demands = demands, + }; + + public DevopsPool FromDemands(params string[] demands) => + new() + { + Demands = demands + .Select(TextExpressions.Raw) + .ToArray(), + }; + + public DevopsPool From( + TextExpression? name = null, + TextExpression? hostedImageName = null, + IEnumerable? demands = null) => + new() + { + Name = name, + HostedImage = hostedImageName, + Demands = demands?.ToArray() ?? [], + }; + + public DevopsPool From( + string? name = null, + string? hostedImageName = null, + IEnumerable? demands = null) => + new() + { + Name = name is null + ? null + : TextExpressions.Raw(name), + HostedImage = hostedImageName is null + ? null + : TextExpressions.Raw(hostedImageName), + Demands = demands + ?.Select(TextExpressions.Raw) + .ToArray() ?? [], + }; + } + + [PublicAPI] + public sealed class DevopsVariableGroupOptions + { + public DevopsVariableGroup Atom => field ??= new("Atom"); + } + + [PublicAPI] + public sealed class DevopsStepsOptions + { + public DevopsCheckoutStep Checkout(DevopsCheckoutStep step) => + step; + } + + [PublicAPI] + public sealed class DevopsOptions + { + internal static DevopsOptions Instance => field ??= new(); + + public DevopsPoolOptions DevopsPool => field ??= new(); + + public DevopsVariableGroupOptions VariableGroup => field ??= new(); + + public DevopsStepsOptions Steps => field ??= new(); + + public ProvideDevopsRunIdAsWorkflowId ProvideDevopsRunIdAsWorkflowId => field ??= new(); + } + + extension(BuildOptions) + { + [PublicAPI] + public static DevopsOptions Devops => DevopsOptions.Instance; + } +} diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Extensions/DevopsWorkflowLabelsExtensions.cs b/src/Invex.Atom.Module.DevopsWorkflows/Extensions/DevopsWorkflowLabelsExtensions.cs new file mode 100644 index 00000000..bc833903 --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/Extensions/DevopsWorkflowLabelsExtensions.cs @@ -0,0 +1,48 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Extensions; + +[PublicAPI] +[SuppressMessage("Performance", "CA1822:Mark members as static")] +public static class DevopsWorkflowLabelsExtensions +{ + [PublicAPI] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public sealed class Pool + { + // Windows + public string Windows_2025_Vs2026 => "windows-2025-vs2026"; + + public string Windows_Latest => "windows-latest"; + + public string Windows_2025 => "windows-2025"; + + public string Windows_2022 => "windows-2022"; + + // Linux + public string Ubuntu_Latest => "ubuntu-latest"; + + public string Ubuntu_24_04 => "ubuntu-24.04"; + + public string Ubuntu_22_04 => "ubuntu-22.04"; + + // MacOS + public string MacOs_Latest => "macos-latest"; + + public string MacOs_15 => "macos-15"; + + public string MacOs_14 => "macos-14"; + } + + [PublicAPI] + public sealed class DevopsLabels + { + internal static DevopsLabels Instance => field ??= new(); + + public Pool Pool => field ??= new(); + } + + extension(WorkflowLabels) + { + [PublicAPI] + public static DevopsLabels Devops => DevopsLabels.Instance; + } +} diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Extensions/DevopsWorkflowTypesExtensions.cs b/src/Invex.Atom.Module.DevopsWorkflows/Extensions/DevopsWorkflowTypesExtensions.cs new file mode 100644 index 00000000..bdc72855 --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/Extensions/DevopsWorkflowTypesExtensions.cs @@ -0,0 +1,20 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Extensions; + +[PublicAPI] +[SuppressMessage("Performance", "CA1822:Mark members as static")] +public static class DevopsWorkflowTypesExtensions +{ + [PublicAPI] + public sealed class Types + { + internal static Types Instance => field ??= new(); + + public DevopsWorkflowType Pipeline => field ??= new(); + } + + extension(WorkflowTypes) + { + [PublicAPI] + public static Types Devops => Types.Instance; + } +} diff --git a/DecSm.Atom.Module.DevopsWorkflows/IDevopsWorkflows.cs b/src/Invex.Atom.Module.DevopsWorkflows/IDevopsWorkflows.cs similarity index 74% rename from DecSm.Atom.Module.DevopsWorkflows/IDevopsWorkflows.cs rename to src/Invex.Atom.Module.DevopsWorkflows/IDevopsWorkflows.cs index 60613c83..44b1a1a0 100644 --- a/DecSm.Atom.Module.DevopsWorkflows/IDevopsWorkflows.cs +++ b/src/Invex.Atom.Module.DevopsWorkflows/IDevopsWorkflows.cs @@ -1,13 +1,14 @@ -namespace DecSm.Atom.Module.DevopsWorkflows; +namespace Invex.Atom.Module.DevopsWorkflows; /// -/// Provides integration with Azure DevOps Pipelines for DecSm.Atom builds. +/// Provides integration with Azure DevOps Pipelines for Invex.Atom builds. /// /// /// Implementing this interface configures the necessary services for generating /// Azure DevOps Pipeline YAML files, providing Azure DevOps-specific workflow variables, /// and adapting build paths and reporting when running within Azure DevOps Pipelines. /// +[PublicAPI] [ConfigureHostBuilder] public partial interface IDevopsWorkflows : IJobRunsOn { @@ -16,21 +17,27 @@ public partial interface IDevopsWorkflows : IJobRunsOn /// /// The host application builder. /// - /// This method registers for generating workflow files + /// This method registers for generating workflow files /// and for Azure DevOps-specific workflow variables. /// When running inside Azure DevOps Pipelines, it also sets up /// for reporting and adjusts artifact and publish paths to Azure DevOps conventions. /// - protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) + protected static partial void ConfigureBuilderFromIDevopsWorkflows(IHostApplicationBuilder builder) { builder.Services.TryAddEnumerable(new ServiceDescriptor(typeof(IWorkflowWriter), - typeof(DevopsWorkflowWriter), + typeof(DevopsWorkflowFileWriter), ServiceLifetime.Singleton)); - builder.Services.TryAddEnumerable(new ServiceDescriptor(typeof(IWorkflowVariableProvider), + builder.Services.TryAddEnumerable(new ServiceDescriptor(typeof(IVariableProvider), typeof(DevopsVariableProvider), ServiceLifetime.Singleton)); + builder.Services.TryAddEnumerable(new ServiceDescriptor(typeof(IWorkflowContextProvider), + typeof(DevopsWorkflowContextProvider), + ServiceLifetime.Singleton)); + + builder.Services.TryAddSingleton(); + if (Devops.IsDevopsPipelines) builder .Services diff --git a/DecSm.Atom.Module.DevopsWorkflows/DecSm.Atom.Module.DevopsWorkflows.csproj b/src/Invex.Atom.Module.DevopsWorkflows/Invex.Atom.Module.DevopsWorkflows.csproj similarity index 65% rename from DecSm.Atom.Module.DevopsWorkflows/DecSm.Atom.Module.DevopsWorkflows.csproj rename to src/Invex.Atom.Module.DevopsWorkflows/Invex.Atom.Module.DevopsWorkflows.csproj index b413ecbf..b7da2576 100644 --- a/DecSm.Atom.Module.DevopsWorkflows/DecSm.Atom.Module.DevopsWorkflows.csproj +++ b/src/Invex.Atom.Module.DevopsWorkflows/Invex.Atom.Module.DevopsWorkflows.csproj @@ -5,6 +5,11 @@ + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -20,12 +25,12 @@ - - + + - + \ No newline at end of file diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Invex.Atom.Module.DevopsWorkflows.props b/src/Invex.Atom.Module.DevopsWorkflows/Invex.Atom.Module.DevopsWorkflows.props new file mode 100644 index 00000000..feba533a --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/Invex.Atom.Module.DevopsWorkflows.props @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/DecSm.Atom.Module.DevopsWorkflows/ProvideDevopsRunIdAsWorkflowId.cs b/src/Invex.Atom.Module.DevopsWorkflows/ProvideDevopsRunIdAsWorkflowId.cs similarity index 66% rename from DecSm.Atom.Module.DevopsWorkflows/ProvideDevopsRunIdAsWorkflowId.cs rename to src/Invex.Atom.Module.DevopsWorkflows/ProvideDevopsRunIdAsWorkflowId.cs index da2c1c18..9fd91ae7 100644 --- a/DecSm.Atom.Module.DevopsWorkflows/ProvideDevopsRunIdAsWorkflowId.cs +++ b/src/Invex.Atom.Module.DevopsWorkflows/ProvideDevopsRunIdAsWorkflowId.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.DevopsWorkflows; +namespace Invex.Atom.Module.DevopsWorkflows; /// /// Represents a workflow option to use the Azure DevOps run ID as the workflow ID. @@ -8,4 +8,4 @@ /// will be used as the unique identifier for the workflow run. /// [PublicAPI] -public sealed record ProvideDevopsRunIdAsWorkflowId : ToggleWorkflowOption; +public sealed record ProvideDevopsRunIdAsWorkflowId : ToggleBuildOption; diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Devops/DevopsWorkflowBuilder.cs b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Devops/DevopsWorkflowBuilder.cs new file mode 100644 index 00000000..09327d1c --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Devops/DevopsWorkflowBuilder.cs @@ -0,0 +1,971 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Workflows.Devops; + +internal sealed partial class DevopsWorkflowBuilder( + IBuildDefinition buildDefinition, + BuildModel buildModel, + IParamService paramService, + IRootedFileSystem fileSystem, + AtomProjectData atomProjectData, + ILogger logger +) +{ + private readonly DevopsExpressionFormatter _expressionFormatter = new(); + + public DevopsPipeline Build(WorkflowModel workflow) => + new DevopsPipeline.DevopsPipelineWithJobs + { + Name = TextExpressions.Raw(workflow.Name), + Jobs = workflow + .Jobs + .Select(x => BuildJob(workflow, x)) + .ToList(), + Trigger = BuildTrigger(workflow), + Pr = BuildPr(workflow), + Parameters = BuildParameters(workflow), + Variables = BuildVariables(workflow), + }; + + private static Trigger? BuildTrigger(WorkflowModel workflow) + { + var pushTriggers = workflow + .Triggers + .OfType() + .ToArray(); + + if (pushTriggers.Length is 0) + return new Trigger.None(); + + // Combine all push triggers into a single full trigger + var includedBranches = pushTriggers + .SelectMany(t => t.IncludedBranches) + .ToArray(); + + var excludedBranches = pushTriggers + .SelectMany(t => t.ExcludedBranches) + .ToArray(); + + var includedPaths = pushTriggers + .SelectMany(t => t.IncludedPaths) + .ToArray(); + + var excludedPaths = pushTriggers + .SelectMany(t => t.ExcludedPaths) + .ToArray(); + + var includedTags = pushTriggers + .SelectMany(t => t.IncludedTags) + .ToArray(); + + var excludedTags = pushTriggers + .SelectMany(t => t.ExcludedTags) + .ToArray(); + + var hasBranches = includedBranches.Length > 0 || excludedBranches.Length > 0; + var hasPaths = includedPaths.Length > 0 || excludedPaths.Length > 0; + var hasTags = includedTags.Length > 0 || excludedTags.Length > 0; + + if (!hasBranches && !hasPaths && !hasTags) + { + if (includedBranches.Length > 0) + return new Trigger.BranchList + { + Branches = new(includedBranches.Select(TextExpressions.Raw)), + }; + + return null; + } + + return new Trigger.Full + { + Branches = hasBranches + ? new IncludeExcludeFilters + { + Include = includedBranches.Length > 0 + ? new TextExpressionCollection(includedBranches.Select(TextExpressions.Raw)) + : null, + Exclude = excludedBranches.Length > 0 + ? new TextExpressionCollection(excludedBranches.Select(TextExpressions.Raw)) + : null, + } + : null, + Paths = hasPaths + ? new IncludeExcludeFilters + { + Include = includedPaths.Length > 0 + ? new TextExpressionCollection(includedPaths.Select(TextExpressions.Raw)) + : null, + Exclude = excludedPaths.Length > 0 + ? new TextExpressionCollection(excludedPaths.Select(TextExpressions.Raw)) + : null, + } + : null, + Tags = hasTags + ? new IncludeExcludeFilters + { + Include = includedTags.Length > 0 + ? new TextExpressionCollection(includedTags.Select(TextExpressions.Raw)) + : null, + Exclude = excludedTags.Length > 0 + ? new TextExpressionCollection(excludedTags.Select(TextExpressions.Raw)) + : null, + } + : null, + }; + } + + private static Pr? BuildPr(WorkflowModel workflow) + { + var prTriggers = workflow + .Triggers + .OfType() + .ToArray(); + + if (prTriggers.Length is 0) + return null; + + var includedBranches = prTriggers + .SelectMany(t => t.IncludedBranches) + .ToArray(); + + var excludedBranches = prTriggers + .SelectMany(t => t.ExcludedBranches) + .ToArray(); + + var includedPaths = prTriggers + .SelectMany(t => t.IncludedPaths) + .ToArray(); + + var excludedPaths = prTriggers + .SelectMany(t => t.ExcludedPaths) + .ToArray(); + + var hasBranches = includedBranches.Length > 0 || excludedBranches.Length > 0; + var hasPaths = includedPaths.Length > 0 || excludedPaths.Length > 0; + + if (!hasBranches && !hasPaths) + return null; + + if (!hasPaths && includedBranches.Length > 0 && excludedBranches.Length is 0) + return new Pr.BranchList + { + Branches = new(includedBranches.Select(TextExpressions.Raw)), + }; + + return new Pr.Full + { + Branches = hasBranches + ? new IncludeExcludeFilters + { + Include = includedBranches.Length > 0 + ? new TextExpressionCollection(includedBranches.Select(TextExpressions.Raw)) + : null, + Exclude = excludedBranches.Length > 0 + ? new TextExpressionCollection(excludedBranches.Select(TextExpressions.Raw)) + : null, + } + : null, + Paths = hasPaths + ? new IncludeExcludeFilters + { + Include = includedPaths.Length > 0 + ? new TextExpressionCollection(includedPaths.Select(TextExpressions.Raw)) + : null, + Exclude = excludedPaths.Length > 0 + ? new TextExpressionCollection(excludedPaths.Select(TextExpressions.Raw)) + : null, + } + : null, + }; + } + + private List? BuildParameters(WorkflowModel workflow) + { + var manualTrigger = workflow + .Triggers + .OfType() + .FirstOrDefault(); + + if (manualTrigger?.Inputs is not { Count: > 0 }) + return null; + + return manualTrigger + .Inputs + .Select(input => + { + var inputParamName = buildDefinition.ParamDefinitions.FirstOrDefault(x => x.Value.ArgName == input.Name) + .Key; + + if (inputParamName is null) + throw new InvalidOperationException( + $"Workflow {workflow.Name} has a manual trigger input named {input.Name} that does not correspond to any parameter in the build definition"); + + switch (input) + { + case ManualBoolInput boolInput: + { + bool? defaultBoolValue = null; + + if (boolInput.DefaultValue.HasValue) + defaultBoolValue = boolInput.DefaultValue.Value; + else + using (paramService.CreateDefaultValuesOnlyScope()) + defaultBoolValue = buildDefinition.AccessParam(inputParamName) switch + { + bool boolParam => boolParam, + string stringParam when bool.TryParse(stringParam, out var parsedBool) => + parsedBool, + _ => defaultBoolValue, + }; + + return new() + { + Name = TextExpressions.Raw(input.Name), + DisplayName = TextExpressions.Raw($"{input.Name} | {input.Description}"), + Type = TextExpressions.Raw("boolean"), + Default = defaultBoolValue is not null + ? TextExpressions.Raw(defaultBoolValue.Value + ? "true" + : "false") + : null, + }; + } + + case ManualChoiceInput choiceInput: + { + using (paramService.CreateDefaultValuesOnlyScope()) + { + var defaultChoiceValue = choiceInput.DefaultValue is { Length: > 0 } + ? choiceInput.DefaultValue + : buildDefinition + .AccessParam(inputParamName) + ?.ToString(); + + return new() + { + Name = TextExpressions.Raw(input.Name), + DisplayName = TextExpressions.Raw($"{input.Name} | {input.Description}"), + Type = TextExpressions.Raw("string"), + Default = defaultChoiceValue is { Length: > 0 } + ? TextExpressions.Raw(defaultChoiceValue) + : null, + Values = new(choiceInput.Choices.Select(TextExpression (c) => TextExpressions.Raw(c))), + }; + } + } + + case ManualStringInput stringInput: + { + using (paramService.CreateDefaultValuesOnlyScope()) + { + var defaultStringValue = stringInput.DefaultValue is { Length: > 0 } + ? stringInput.DefaultValue + : buildDefinition + .AccessParam(inputParamName) + ?.ToString(); + + return new() + { + Name = TextExpressions.Raw(input.Name), + DisplayName = TextExpressions.Raw($"{input.Name} | {input.Description}"), + Type = TextExpressions.Raw("string"), + Default = defaultStringValue is { Length: > 0 } + ? TextExpressions.Raw(defaultStringValue) + : null, + }; + } + } + + default: + throw new ArgumentOutOfRangeException(nameof(input)); + } + }) + .ToList(); + } + + private static Variables.VariableList? BuildVariables(WorkflowModel workflow) + { + var variableGroups = workflow + .Options + .OfType() + .ToArray(); + + if (variableGroups.Length is 0) + return null; + + return new() + { + Values = variableGroups + .Select(Variable (g) => new Variable.Group + { + GroupName = TextExpressions.Raw(g.Name), + }) + .ToList(), + }; + } + + private Job BuildJob(WorkflowModel workflow, WorkflowJobModel job) + { + var poolOption = job + .TargetStep + .Options + .Concat(workflow.Options) + .OfType() + .FirstOrDefault(); + + var pool = BuildPool(poolOption); + + var condition = TargetCondition + .GetOptions(job.TargetStep.Options) + .ToList(); + + var conditionExpression = condition switch + { + { Count: > 1 } => condition[0] + .Condition + .And(condition + .Skip(1) + .Select(x => x.Condition) + .ToArray()), + [_] => condition[0].Condition, + _ => null, + }; + + var environment = DeployToEnvironment.Get(job.TargetStep.Options); + var jobVariables = BuildJobConsumedVariables(job); + + if (environment is not null) + return new Job.Deployment + { + DeploymentId = TextExpressions.Raw(job.Name), + DisplayName = TextExpressions.Raw(job.Name), + DependsOn = job.JobDependencies.Count > 0 + ? new TextExpressionCollection(job.JobDependencies.Select(TextExpressions.Raw)) + : null, + Condition = conditionExpression, + Pool = pool, + Variables = jobVariables, + Environment = new DeploymentEnvironment.EnvironmentName + { + Name = environment.EnvironmentName, + }, + Strategy = new DeploymentStrategy.RunOnce + { + Deploy = new() + { + Steps = BuildSteps(workflow, job), + }, + }, + }; + + return new Job.RegularJob + { + JobId = TextExpressions.Raw(job.Name), + DisplayName = TextExpressions.Raw(job.Name), + DependsOn = job.JobDependencies.Count > 0 + ? new TextExpressionCollection(job.JobDependencies.Select(TextExpressions.Raw)) + : null, + Condition = conditionExpression, + Pool = pool, + Variables = jobVariables, + Strategy = job.TargetStep.MatrixDimensions.Count > 0 + ? new JobStrategy + { + Matrix = BuildMatrix(job.TargetStep.MatrixDimensions), + } + : null, + Steps = BuildSteps(workflow, job), + }; + } + + private Variables.VariableList? BuildJobConsumedVariables(WorkflowJobModel job) + { + var target = buildModel.GetTarget(job.TargetStep.Name); + + var targets = new List + { + target, + }; + + if (UseCustomArtifactProvider.Get(job.TargetStep.Options) is { Enabled: true }) + { + if (target.ConsumedArtifacts.Count > 0) + targets.Add(buildModel.GetTarget(nameof(IRetrieveArtifact.RetrieveArtifact))); + + if (target.ProducedArtifacts.Count > 0 && + SuppressArtifactPublishingOption.Get(job.TargetStep.Options) is not { Enabled: true }) + targets.Add(buildModel.GetTarget(nameof(IStoreArtifact.StoreArtifact))); + } + + var consumedVariables = targets + .SelectMany(t => t.ConsumedVariables) + .DistinctBy(v => v.VariableName) + .ToList(); + + if (consumedVariables.Count is 0) + return null; + + return new() + { + Values = consumedVariables.ConvertAll(consumedVariable => + { + var argName = buildDefinition.ParamDefinitions[consumedVariable.VariableName].ArgName; + + return new Variable.Name + { + VariableName = TextExpressions.Raw(argName), + Value = new DevopsRuntimeExpression(new RawExpression( + $"dependencies.{consumedVariable.TargetName}.outputs['{consumedVariable.TargetName}.{argName}']")), + }; + }), + }; + } + + private static Pool.PoolSpec BuildPool(DevopsPool? poolOption) + { + if (poolOption is null) + return new() + { + VmImage = TextExpressions.Raw("ubuntu-latest"), + }; + + if (poolOption.HostedImage is not null) + return new() + { + VmImage = poolOption.HostedImage, + Name = poolOption.Name, + Demands = poolOption.Demands.Count > 0 + ? poolOption.Demands + : null, + }; + + if (poolOption.Name is not null) + return new() + { + Name = poolOption.Name, + Demands = poolOption.Demands.Count > 0 + ? poolOption.Demands + : null, + }; + + return new() + { + VmImage = TextExpressions.Raw("ubuntu-latest"), + }; + } + + private Dictionary> BuildMatrix( + IReadOnlyList matrixDimensions) + { + var dimensions = matrixDimensions + .Select(d => (buildDefinition.ParamDefinitions[d.Name].ArgName, d.Values)) + .ToArray(); + + var result = new Dictionary>(); + var indices = new int[dimensions.Length]; + var counter = 1; + + while (true) + { + // Compute the current dimension values and sanitize for the combination name + var currentValues = indices + .Select((idx, i) => dimensions[i] + .Values[idx]) + .ToArray(); + + var sanitizedValues = currentValues + .Select(v => new string(_expressionFormatter + .Format(v) + .Select(c => char.IsLetterOrDigit(c) + ? c + : '-') + .ToArray())) + .Select(v => HyphenReductionRegex() + .Replace(v, "-")) + .Select(v => v.Trim('-')) + .ToArray(); + + var combinationName = $"{counter++:D3}_{string.Join("_", sanitizedValues)}"; + + var entry = new Dictionary(); + + for (var i = 0; i < dimensions.Length; i++) + entry[dimensions[i].ArgName] = currentValues[i]; + + result[combinationName] = entry; + + // Advance indices (odometer-style) + var dim = 0; + + while (dim < dimensions.Length) + { + if (indices[dim] < dimensions[dim].Values.Count - 1) + { + indices[dim]++; + + break; + } + + indices[dim] = 0; + dim++; + } + + if (dim == dimensions.Length) + break; + } + + return result; + } + + [GeneratedRegex("-+")] + private static partial Regex HyphenReductionRegex(); + + private List BuildSteps(WorkflowModel workflow, WorkflowJobModel job) + { + var additionalSteps = IAdditionalStepOption + .GetOptions(job.TargetStep.Options) + .ToList(); + + // Special case: Add default checkout step if there isn't one + if (!additionalSteps.Any(x => x is CheckoutStep)) + additionalSteps.Add(new DevopsCheckoutStep + { + EnableRun = true, + Repository = TextExpressions.Raw("self"), + FetchDepth = TextExpressions.From(0), + }); + + additionalSteps = additionalSteps.ToList(); + + // Add pre-target additional steps + var steps = new List(additionalSteps + .Where(x => x.Order < 0) + .OrderBy(x => x.Order) + .Select(BuildAdditionalStep)); + + // Matrix params + var matrixParams = job + .TargetStep + .MatrixDimensions + .Select(dimension => buildDefinition.ParamDefinitions[dimension.Name].ArgName) + .Select(name => (Name: name, Value: $"$({name})")) + .ToList(); + + var buildSliceValue = matrixParams switch + { + { Count: 0 } => null, + { Count: 1 } => matrixParams[0].Value, + { Count: > 1 } => string.Join("-", matrixParams.Select(x => x.Value)), + }; + + if (buildSliceValue is not null) + matrixParams.Add(("build-slice", buildSliceValue)); + + var target = buildModel.GetTarget(job.TargetStep.Name); + + // Consume artifacts + if (target.ConsumedArtifacts.Count > 0) + { + foreach (var consumedArtifact in target.ConsumedArtifacts) + { + var consumedStep = workflow + .Jobs + .Select(x => x.TargetStep) + .Single(x => x.Name == consumedArtifact.TargetName); + + if (SuppressArtifactPublishingOption.Get(consumedStep.Options) is { Enabled: true }) + logger.LogWarning( + "Workflow {WorkflowName} target {TargetName} consumes artifact {ArtifactName} from target {SourceTargetName}, which has artifact publishing suppressed; this may cause the workflow to fail", + workflow.Name, + job.TargetStep.Name, + consumedArtifact.ArtifactName, + consumedArtifact.TargetName); + } + + if (UseCustomArtifactProvider.Get(job.TargetStep.Options) is { Enabled: true }) + foreach (var slice in target.ConsumedArtifacts.GroupBy(a => a.BuildSlice)) + { + var artifactNames = slice + .Select(x => x.ArtifactName) + .ToArray(); + + var retrieveTarget = buildModel.GetTarget(nameof(IRetrieveArtifact.RetrieveArtifact)); + + var env = BuildTargetStepEnv(workflow, + job, + retrieveTarget.Params, + retrieveTarget.ConsumedVariables, + matrixParams); + + env["atom-artifacts"] = TextExpressions.Raw(string.Join(",", artifactNames)); + + if (!env.ContainsKey("build-slice")) + { + if (slice.Key is { Length: > 0 }) + env.Add("build-slice", TextExpressions.Raw(slice.Key)); + else if (buildSliceValue is not null) + env.Add("build-slice", TextExpressions.Raw(buildSliceValue)); + } + + steps.Add(BuildScriptStep(artifactNames switch + { + [var name] => $"Retrieve artifact `{name}`", + _ => artifactNames.Length < 60 + ? $"Retrieve artifacts `{string.Join(", ", artifactNames)}`" + : "Retrieve multiple artifacts", + }, + "RetrieveArtifact", + env)); + } + else + foreach (var artifact in target.ConsumedArtifacts) + { + var artifactName = artifact.BuildSlice is { Length: > 0 } + ? $"{artifact.ArtifactName}-{artifact.BuildSlice}" + : buildSliceValue is not null + ? $"{artifact.ArtifactName}-{buildSliceValue}" + : artifact.ArtifactName; + + steps.Add(new Step.Task + { + TaskName = TextExpressions.Raw("DownloadPipelineArtifact@2"), + DisplayName = TextExpressions.Raw(artifact.ArtifactName), + Inputs = new Dictionary + { + ["artifact"] = TextExpressions.Raw(artifactName), + ["path"] = TextExpressions.Raw( + $"{DevopsWorkflows.Devops.PipelineArtifactDirectory}/{artifact.ArtifactName}"), + }, + }); + } + } + + // Target step + var targetStepEnv = BuildTargetStepEnv(workflow, job, target.Params, target.ConsumedVariables, matrixParams); + + steps.Add(BuildScriptStep(job.TargetStep.Name, job.TargetStep.Name, targetStepEnv, job.TargetStep.Name)); + + // Produce artifacts + if (target.ProducedArtifacts.Count > 0 && + SuppressArtifactPublishingOption.Get(job.TargetStep.Options) is not { Enabled: true }) + { + if (UseCustomArtifactProvider.Get(job.TargetStep.Options) is { Enabled: true }) + foreach (var slice in target.ProducedArtifacts.GroupBy(a => a.BuildSlice)) + { + var artifactNames = slice + .Select(x => x.ArtifactName) + .ToArray(); + + var storeTarget = buildModel.GetTarget(nameof(IStoreArtifact.StoreArtifact)); + + var env = BuildTargetStepEnv(workflow, + job, + storeTarget.Params, + storeTarget.ConsumedVariables, + matrixParams); + + env["atom-artifacts"] = TextExpressions.Raw(string.Join(",", artifactNames)); + + if (!env.ContainsKey("build-slice")) + { + if (slice.Key is { Length: > 0 }) + env.Add("build-slice", TextExpressions.Raw(slice.Key)); + else if (buildSliceValue is not null) + env.Add("build-slice", TextExpressions.Raw(buildSliceValue)); + } + + steps.Add(BuildScriptStep(artifactNames switch + { + [var name] => $"Store artifact `{name}`", + _ => artifactNames.Length < 60 + ? $"Store artifacts `{string.Join(", ", artifactNames)}`" + : "Store multiple artifacts", + }, + "StoreArtifact", + env)); + } + else + foreach (var artifact in target.ProducedArtifacts) + { + var artifactName = artifact.BuildSlice is { Length: > 0 } + ? $"{artifact.ArtifactName}-{artifact.BuildSlice}" + : buildSliceValue is not null + ? $"{artifact.ArtifactName}-{buildSliceValue}" + : artifact.ArtifactName; + + steps.Add(new Step.Task + { + TaskName = TextExpressions.Raw("PublishPipelineArtifact@1"), + DisplayName = TextExpressions.Raw(artifact.ArtifactName), + Inputs = new Dictionary + { + ["artifactName"] = TextExpressions.Raw(artifactName), + ["targetPath"] = TextExpressions.Raw( + $"{DevopsWorkflows.Devops.PipelinePublishDirectory}/{artifact.ArtifactName}"), + }, + }); + } + } + + // Add post-target additional steps + steps.AddRange(additionalSteps + .Where(x => x.Order > 0) + .OrderBy(x => x.Order) + .Select(BuildAdditionalStep)); + + return steps; + } + + private static Step BuildAdditionalStep(IAdditionalStepOption additionalStep) => + additionalStep switch + { + IDevopsAdditionalStepOption devopsStep => devopsStep.Build(), + SetupDotnetStep setupDotnetStep => BuildSetupDotnetStep(setupDotnetStep), + AddNugetFeedsStep addNugetFeedsStep => BuildAddNugetFeedsStep(addNugetFeedsStep), + _ => throw new InvalidOperationException( + $"Unknown additional step type: {additionalStep.GetType().FullName}"), + }; + + private static Step.Task BuildSetupDotnetStep(SetupDotnetStep step) + { + var inputs = new Dictionary(); + + if (step.DotnetVersion is not null) + inputs["version"] = step.DotnetVersion; + + return new() + { + TaskName = TextExpressions.Raw("UseDotNet@2"), + DisplayName = step.DotnetVersion is not null + ? TextExpressions.Concat(["Setup .NET ", step.DotnetVersion]) + : TextExpressions.Raw("Setup .NET"), + Inputs = inputs.Count > 0 + ? inputs + : null, + }; + } + + private static Step.Script BuildAddNugetFeedsStep(AddNugetFeedsStep step) + { + var feedsToAdd = step.FeedsToAdd.ToList(); + + var toolVersion = ""; + + if (step.SyncAtomToolVersionToLibraryVersion) + { + if (SemVer.TryParse(typeof(AtomHost).Assembly.GetCustomAttribute() + ?.InformationalVersion ?? + "", + out var semVer)) + toolVersion = + SemVer.Parse($"{semVer.Prefix}{(semVer.IsPreRelease ? $"-{semVer.PreRelease}" : string.Empty)}"); + else + throw new InvalidOperationException( + "Failed to parse Invex.Atom.Host assembly version as SemVer for syncing atom tool version"); + } + + var env = feedsToAdd.ToDictionary( + k => AddNugetFeedsStep.GetEnvVarNameForFeed(k.FeedName), + v => TextExpressions.Raw($"$({v.SecretName})")); + + // If we are using .net 10+ then we can use the dotnet tool exec command instead of installing the tool to run it + if (SemVer.TryParse(RuntimeInformation + .FrameworkDescription + .Replace(".NET ", "") + .Replace("x", "0"), + out var version) && + version.Major >= 10) + return new() + { + ScriptContent = TextExpressions.Raw(string.Join("\n", + feedsToAdd.Select(feedToAdd => step.SyncAtomToolVersionToLibraryVersion + ? $"dotnet tool exec invex.atom.tool@{toolVersion} -y -- nuget-add --name \"{feedToAdd.FeedName}\" --url \"{feedToAdd.FeedUrl}\"" + : $"dotnet tool exec invex.atom.tool -y -- nuget-add --name \"{feedToAdd.FeedName}\" --url \"{feedToAdd.FeedUrl}\""))), + DisplayName = TextExpressions.Raw("Setup NuGet"), + Env = env.Count > 0 + ? env + : null, + }; + + return new() + { + ScriptContent = TextExpressions.Raw(string.Join("\n", + feedsToAdd + .Select(feedToAdd => step.SyncAtomToolVersionToLibraryVersion + ? $"atom nuget-add --name \"{feedToAdd.FeedName}\" --url \"{feedToAdd.FeedUrl}\" --tool-version \"{toolVersion}\"" + : $"atom nuget-add --name \"{feedToAdd.FeedName}\" --url \"{feedToAdd.FeedUrl}\"") + .Prepend("dotnet tool update --global Invex.Atom.Tool"))), + DisplayName = TextExpressions.Raw("Setup NuGet"), + Env = env.Count > 0 + ? env + : null, + }; + } + + private Step.Script BuildScriptStep( + string displayName, + string targetName, + Dictionary env, + string? stepName = null) + { + var runScript = GetRunScript(targetName); + + return new() + { + ScriptContent = TextExpressions.Raw(runScript), + DisplayName = TextExpressions.Raw(displayName), + Name = stepName is not null + ? TextExpressions.Raw(stepName) + : null, + Env = env.Count > 0 + ? env + : null, + }; + } + + private string GetRunScript(string targetName) + { + if (atomProjectData.IsFileBasedApp) + { + if (AppContext.GetData("EntryPointFilePath") is not string fileName) + throw new InvalidOperationException( + "AtomFileSystem reports file-based app but AppContext.EntryPointFilePath is null, cannot determine file path to run"); + + var filePathRelativeToRoot = + fileSystem.FileSystem.Path.GetRelativePath(fileSystem.AtomRootDirectory, fileName); + + return $"dotnet run --file {filePathRelativeToRoot} {targetName} --skip --headless"; + } + + var projectPath = FindProjectPath(fileSystem, atomProjectData.ProjectName); + + return $"dotnet run --project {projectPath} {targetName} --skip --headless"; + } + + private Dictionary BuildTargetStepEnv( + WorkflowModel workflow, + WorkflowJobModel job, + IReadOnlyList usedParams, + IReadOnlyList consumedVariables, + List<(string Name, string Value)> matrixParams) + { + var targetStepEnv = new Dictionary(); + + foreach (var manualTrigger in workflow.Triggers.OfType()) + foreach (var input in manualTrigger.Inputs?.Where(i => usedParams + .Select(p => p.Param.ArgName) + .Any(p => p == i.Name)) ?? []) + targetStepEnv[input.Name] = TextExpressions + .Raw("parameters")[input.Name] + .Evaluate(); + + foreach (var consumedVariable in consumedVariables) + { + var argName = buildDefinition.ParamDefinitions[consumedVariable.VariableName].ArgName; + + // Consumed variables are declared as job-level variables (using runtime expressions) in BuildJobConsumedVariables. + // Here we reference them via the macro $(varName) syntax, which is the correct way to read job variables in step envs. + targetStepEnv[argName] = new DevopsMacroExpression(TextExpressions.Raw(argName)); + } + + var requiredSecrets = usedParams + .Where(x => x.Param.IsSecret) + .ToArray(); + + if (requiredSecrets.Length > 0) + { + foreach (var injectedSecret in workflow.Options.OfType()) + if (buildDefinition.ParamDefinitions.GetValueOrDefault(injectedSecret.SecretName) is + { } paramDefinition) + targetStepEnv[paramDefinition.ArgName] = + new DevopsMacroExpression(TextExpressions.Raw(paramDefinition.EnvVarName)); + + foreach (var injectedEnvVar in workflow.Options.OfType()) + if (buildDefinition.ParamDefinitions.GetValueOrDefault(injectedEnvVar.SecretName) is + { } paramDefinition) + targetStepEnv[paramDefinition.ArgName] = + new DevopsMacroExpression(TextExpressions.Raw(paramDefinition.EnvVarName)); + } + + foreach (var requiredSecret in requiredSecrets) + if (WorkflowSecretInjection + .GetOptions(job.TargetStep.Options) + .Any(x => x.Value == requiredSecret.Param.Name)) + targetStepEnv[requiredSecret.Param.ArgName] = + new DevopsMacroExpression(TextExpressions.Raw(requiredSecret.Param.EnvVarName)); + + var environmentInjections = WorkflowParamInjectionFromEnvironment.GetOptions(job.TargetStep.Options); + var paramInjections = WorkflowParamInjection.GetOptions(job.TargetStep.Options); + var environmentVariableInjections = WorkflowEnvironmentVariableInjection.GetOptions(job.TargetStep.Options); + + environmentInjections = environmentInjections + .Where(e => paramInjections.All(p => p.Name != e.Value)) + .ToList(); + + foreach (var environmentInjection in environmentInjections) + { + if (!buildDefinition.ParamDefinitions.TryGetValue(environmentInjection.Value, out var paramDefinition)) + { + logger.LogWarning( + "Workflow {WorkflowName} command {CommandName} has an injection for parameter {ParamName} that does not exist", + workflow.Name, + job.TargetStep.Name, + environmentInjection.Value); + + continue; + } + + targetStepEnv[paramDefinition.ArgName] = + new DevopsMacroExpression(TextExpressions.Raw(paramDefinition.EnvVarName)); + } + + foreach (var paramInjection in paramInjections) + { + if (!buildDefinition.ParamDefinitions.TryGetValue(paramInjection.Name, out var paramDefinition)) + { + logger.LogWarning( + "Workflow {WorkflowName} command {CommandName} has an injection for parameter {ParamName} that is not consumed by the command", + workflow.Name, + job.TargetStep.Name, + paramInjection.Name); + + continue; + } + + targetStepEnv[paramDefinition.ArgName] = paramInjection.InjectionExpression; + } + + foreach (var environmentVariableInjection in environmentVariableInjections) + targetStepEnv[environmentVariableInjection.Name] = environmentVariableInjection.Value; + + foreach (var matrixParam in matrixParams) + targetStepEnv[matrixParam.Name] = TextExpressions.Raw(matrixParam.Value); + + return targetStepEnv; + } + + private static string FindProjectPath(IRootedFileSystem fileSystem, string projectName) + { + var projectPath = fileSystem + .FileSystem + .DirectoryInfo + .New(fileSystem.AtomRootDirectory) + .EnumerateFiles("*.csproj", + new EnumerationOptions + { + IgnoreInaccessible = true, + MaxRecursionDepth = 4, + RecurseSubdirectories = true, + ReturnSpecialDirectories = false, + }) + .FirstOrDefault(f => f.Name.Equals($"{projectName}.csproj", StringComparison.OrdinalIgnoreCase)); + + if (projectPath?.FullName is null) + throw new InvalidOperationException($"Project '{projectName}' not found in current directory."); + + return fileSystem + .FileSystem + .Path + .GetRelativePath(fileSystem.AtomRootDirectory, projectPath.FullName) + .Replace("\\", "/"); + } +} diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Devops/DevopsWorkflowFileWriter.cs b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Devops/DevopsWorkflowFileWriter.cs new file mode 100644 index 00000000..ff72c4da --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Devops/DevopsWorkflowFileWriter.cs @@ -0,0 +1,23 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Workflows.Devops; + +internal sealed class DevopsWorkflowFileWriter( + IRootedFileSystem fileSystem, + DevopsWorkflowBuilder workflowBuilder, + ILogger logger +) : WorkflowFileWriter(fileSystem, logger) +{ + private readonly IRootedFileSystem _fileSystem = fileSystem; + private readonly DevopsPipelineWriter _pipelineWriter = new(); + + protected override string FileExtension => "yml"; + + protected override RootedPath FileLocation => _fileSystem.AtomRootDirectory / ".devops" / "workflows"; + + protected override string WriteWorkflow(WorkflowModel workflow) + { + var devopsPipeline = workflowBuilder.Build(workflow); + _pipelineWriter.Write(devopsPipeline); + + return _pipelineWriter.TextWriter.ToString(); + } +} diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Devops/DevopsWorkflowType.cs b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Devops/DevopsWorkflowType.cs new file mode 100644 index 00000000..553f5a28 --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Devops/DevopsWorkflowType.cs @@ -0,0 +1,7 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Workflows.Devops; + +[PublicAPI] +public sealed record DevopsWorkflowType : IWorkflowType +{ + public bool IsRunning => DevopsWorkflows.Devops.IsDevopsPipelines; +} diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Options/DevopsPool.cs b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Options/DevopsPool.cs new file mode 100644 index 00000000..6c03dd92 --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Options/DevopsPool.cs @@ -0,0 +1,11 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Workflows.Options; + +[PublicAPI] +public sealed record DevopsPool : IBuildOption +{ + public TextExpressionCollection Demands { get; init; } = []; + + public TextExpression? Name { get; init; } + + public TextExpression? HostedImage { get; init; } +} diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Options/DevopsVariableGroup.cs b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Options/DevopsVariableGroup.cs new file mode 100644 index 00000000..6c290b5b --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Options/DevopsVariableGroup.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Workflows.Options; + +[PublicAPI] +public record DevopsVariableGroup(string Name) : IBuildOption; diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Steps/DevopsCheckoutStep.cs b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Steps/DevopsCheckoutStep.cs new file mode 100644 index 00000000..96cac486 --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Steps/DevopsCheckoutStep.cs @@ -0,0 +1,109 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Workflows.Steps; + +/// +/// Represents a workflow option for configuring the checkout step in Azure DevOps Pipelines. +/// +[PublicAPI] +public sealed record DevopsCheckoutStep : CheckoutStep, IDevopsAdditionalStepOption +{ + /// + /// Repository to check out. Valid values: "self" | "none" or repository resource name. + /// + public TextExpression Repository { get; init; } = "self"; + + /// + /// Whether to clean the repository before checkout. + /// + public TextExpression? Clean { get; init; } + + /// + /// Number of commits to fetch (depth). 0 indicates all history. + /// + public TextExpression? FetchDepth { get; init; } + + /// + /// Whether to download Git-LFS files. + /// + public TextExpression? Lfs { get; init; } + + /// + /// Whether to checkout submodules. + /// + public TextExpression? Submodules { get; init; } + + /// + /// Path to check out source code, relative to $(Agent.BuildDirectory). + /// + public TextExpression? Path { get; init; } + + /// + /// Persist credentials for later use by the Git command-line tool. + /// + public TextExpression? PersistCredentials { get; init; } + + /// + /// Evaluate this condition expression to determine whether to run this task. + /// + public TextExpression? Condition { get; init; } + + /// + /// Continue running even on failure? + /// + public TextExpression? ContinueOnError { get; init; } + + /// + /// Human-readable name for the task. + /// + public TextExpression? DisplayName { get; init; } + + /// + /// Environment in which to run this task. + /// + public StepTarget? Target { get; init; } + + /// + /// Run this task when the job runs? + /// + public TextExpression? EnableRun { get; init; } + + /// + /// Variables to map into the process's environment. + /// + public IReadOnlyDictionary? Env { get; init; } + + /// + /// ID of the step. + /// + public TextExpression? Name { get; init; } + + /// + /// Time to wait for this task to complete before the server kills it. + /// + public TextExpression? TimeoutInMinutes { get; init; } + + /// + /// Number of retries if the task fails. + /// + public TextExpression? RetryCountOnTaskFailure { get; init; } + + public Step Build() => + new Step.Checkout + { + Repository = Repository, + Clean = Clean, + FetchDepth = FetchDepth, + Lfs = Lfs, + Submodules = Submodules, + Path = Path, + PersistCredentials = PersistCredentials, + Condition = Condition, + ContinueOnError = ContinueOnError, + DisplayName = DisplayName, + Target = Target, + Enabled = EnableRun, + Env = Env, + Name = Name, + TimeoutInMinutes = TimeoutInMinutes, + RetryCountOnTaskFailure = RetryCountOnTaskFailure, + }; +} diff --git a/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Steps/IDevopsAdditionalStepOption.cs b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Steps/IDevopsAdditionalStepOption.cs new file mode 100644 index 00000000..c47ee4e0 --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/Workflows/Steps/IDevopsAdditionalStepOption.cs @@ -0,0 +1,7 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Workflows.Steps; + +[PublicAPI] +public interface IDevopsAdditionalStepOption : IAdditionalStepOption +{ + Step Build(); +} diff --git a/src/Invex.Atom.Module.DevopsWorkflows/_usings.cs b/src/Invex.Atom.Module.DevopsWorkflows/_usings.cs new file mode 100644 index 00000000..2a7d62be --- /dev/null +++ b/src/Invex.Atom.Module.DevopsWorkflows/_usings.cs @@ -0,0 +1,39 @@ +global using System.Diagnostics.CodeAnalysis; +global using System.Reflection; +global using System.Runtime.InteropServices; +global using System.Text; +global using System.Text.RegularExpressions; +global using Invex.Atom.Build; +global using Invex.Atom.Build.Artifacts; +global using Invex.Atom.Build.BuildOptions; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.FileSystem; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Build.Model; +global using Invex.Atom.Build.Params; +global using Invex.Atom.Build.Reports; +global using Invex.Atom.Build.Util; +global using Invex.Atom.Build.Variables; +global using Invex.Atom.Module.DevopsWorkflows.Workflows.Devops; +global using Invex.Atom.Module.DevopsWorkflows.Workflows.Options; +global using Invex.Atom.Module.DevopsWorkflows.Workflows.Steps; +global using Invex.Atom.Workflows; +global using Invex.Atom.Workflows.Definition; +global using Invex.Atom.Workflows.Definition.Triggers; +global using Invex.Atom.Workflows.Dotnet.Nuget; +global using Invex.Atom.Workflows.Model; +global using Invex.Atom.Workflows.Options; +global using Invex.Atom.Workflows.Options.Injections; +global using Invex.Atom.Workflows.WorkflowContext; +global using Invex.Atom.Workflows.Writer; +global using Invex.StructuredText.AzureDevopsPipelines.DevopsPipelineModel.Jobs; +global using Invex.StructuredText.AzureDevopsPipelines.DevopsPipelineModel.Pipeline; +global using Invex.StructuredText.AzureDevopsPipelines.DevopsPipelineModel.Steps; +global using Invex.StructuredText.AzureDevopsPipelines.DevopsPipelineModel.Supporting; +global using Invex.StructuredText.AzureDevopsPipelines.DevopsPipelineModel.Triggers; +global using Invex.StructuredText.AzureDevopsPipelines.DevopsPipelineModel.Variables; +global using JetBrains.Annotations; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; diff --git a/DecSm.Atom.Module.Dotnet/Cli/DotnetCliOptions.cs b/src/Invex.Atom.Module.Dotnet/Cli/DotnetCliOptions.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/DotnetCliOptions.cs rename to src/Invex.Atom.Module.Dotnet/Cli/DotnetCliOptions.cs index 7ea1d439..21ddffee 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/DotnetCliOptions.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/DotnetCliOptions.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; /// /// Represents a set of options for configuring the execution of .NET CLI commands. diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Add.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Add.cs similarity index 93% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Add.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Add.cs index 9fcd12cd..e1ba75d7 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Add.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Add.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -23,7 +23,7 @@ public Task Add( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET Add Command /// @@ -37,11 +37,11 @@ public Task Add( /// Cancellation token /// public Task Add( - DecSm.Atom.Paths.RootedPath projectOrFile, + Invex.FileSystem.RootedPath projectOrFile, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -55,14 +55,14 @@ public Task Add( "add", projectOrFile, }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Add( - DecSm.Atom.Paths.RootedPath projectOrFile, + Invex.FileSystem.RootedPath projectOrFile, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ) { @@ -70,11 +70,11 @@ public Task Add( "add", projectOrFile, }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/AddPackage.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/AddPackage.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/AddPackage.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/AddPackage.cs index c7a5ae8e..8a3e6f22 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/AddPackage.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/AddPackage.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/AddReference.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/AddReference.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/AddReference.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/AddReference.cs index d7e41de3..59f8fc7a 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/AddReference.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/AddReference.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Build.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Build.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Build.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Build.cs index 8ecf2267..e3359130 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Build.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Build.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -27,7 +27,7 @@ public Task Build( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET Builder /// @@ -49,7 +49,7 @@ public Task Build( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET Builder /// @@ -66,12 +66,12 @@ public Task Build( /// Cancellation token /// public Task Build( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, BuildOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -87,12 +87,12 @@ public Task Build( $"{string.Join(" ", projectOrSolutionOrFile)}", (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Build( System.String projectOrSolutionOrFile, BuildOptions? options = null, @@ -104,14 +104,14 @@ public Task Build( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Build( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, BuildOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default @@ -121,12 +121,12 @@ public Task Build( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } [PublicAPI] @@ -140,7 +140,7 @@ public System.String? Arch get; init; } - + /// /// The artifacts path. All output from the project, including build, publish, and pack output, will go in subfolders under the specified path. /// @@ -149,16 +149,16 @@ public System.String? ArtifactsPath get; init; } - + /// - /// + /// /// public System.String? Configfile { get; init; } - + /// /// The configuration to use for building the project. The default for most projects is 'Debug'. /// @@ -167,13 +167,13 @@ public System.String? Configuration get; init; } - + public System.Boolean? Debug { get; init; } - + /// /// Force the command to ignore any persistent build servers. /// @@ -182,16 +182,16 @@ public System.Boolean? DisableBuildServers get; init; } - + /// - /// + /// /// public System.Boolean? DisableParallel { get; init; } - + /// /// Force all dependencies to be resolved even if the last restore was successful. /// This is equivalent to deleting project.assets.json. @@ -201,7 +201,7 @@ public System.Boolean? Force get; init; } - + /// /// The target framework to build for. The target framework must also be specified in the project file. /// @@ -210,40 +210,40 @@ public System.String? Framework get; init; } - + public System.String[]? GetItem { get; init; } - + public System.String[]? GetProperty { get; init; } - + public System.String[]? GetResultOutputFile { get; init; } - + public System.String[]? GetTargetResult { get; init; } - + /// - /// + /// /// public System.Boolean? IgnoreFailedSources { get; init; } - + /// /// Allows the command to stop and wait for user input or action (for example to complete authentication). /// @@ -252,16 +252,16 @@ public System.Boolean? Interactive get; init; } - + /// - /// + /// /// public System.Boolean? NoCache { get; init; } - + /// /// Do not build project-to-project references and only build the specified project. /// @@ -270,16 +270,16 @@ public System.Boolean? NoDependencies get; init; } - + /// - /// + /// /// public System.Boolean? NoHttpCache { get; init; } - + /// /// Do not use incremental building. /// @@ -288,7 +288,7 @@ public System.Boolean? NoIncremental get; init; } - + /// /// Do not restore the project before building. /// @@ -297,7 +297,7 @@ public System.Boolean? NoRestore get; init; } - + /// /// Publish your application as a framework dependent application. A compatible .NET runtime must be installed on the target machine to run your application. /// @@ -306,7 +306,7 @@ public System.Boolean? NoSelfContained get; init; } - + /// /// Do not display the startup banner or the copyright message. /// @@ -315,7 +315,7 @@ public System.Boolean? Nologo get; init; } - + /// /// The target operating system. /// @@ -324,7 +324,7 @@ public System.String? Os get; init; } - + /// /// The output directory to place built artifacts in. /// @@ -333,28 +333,28 @@ public System.String? Output get; init; } - + /// - /// + /// /// public System.String? Packages { get; init; } - + public System.Collections.Generic.IReadOnlyDictionary? Property { get; init; } - + public System.Collections.Generic.IReadOnlyDictionary? RestoreProperty { get; init; } - + /// /// The target runtime to build for. /// @@ -363,7 +363,7 @@ public System.String? Runtime get; init; } - + /// /// Publish the .NET runtime with your application so the runtime doesn't need to be installed on the target machine. /// The default is 'false.' However, when targeting .NET 7 or lower, the default is 'true' if a runtime identifier is specified. @@ -373,16 +373,16 @@ public System.Boolean? SelfContained get; init; } - + /// - /// + /// /// public System.Collections.Generic.IEnumerable? Source { get; init; } - + /// /// Build these targets in this project. Use a semicolon or a comma to separate multiple targets, or specify each target separately. /// @@ -391,7 +391,7 @@ public System.String[]? Target get; init; } - + /// /// Use current runtime as the target runtime. /// @@ -400,7 +400,7 @@ public System.Boolean? UseCurrentRuntime get; init; } - + /// /// Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]. /// @@ -409,7 +409,7 @@ public VerbosityOptions? Verbosity get; init; } - + /// /// Set the value of the $(VersionSuffix) property to use when building the project. /// @@ -418,7 +418,7 @@ public System.String? VersionSuffix get; init; } - + public override string ToString() { return string.Join(' ', new[] @@ -528,6 +528,6 @@ VersionSuffix is not null } .Where(x => x is { Length: > 0 })); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/BuildServer.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/BuildServer.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/BuildServer.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/BuildServer.cs index 367a30ec..2640f279 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/BuildServer.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/BuildServer.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/BuildServerShutdown.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/BuildServerShutdown.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/BuildServerShutdown.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/BuildServerShutdown.cs index 0f25e242..01886010 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/BuildServerShutdown.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/BuildServerShutdown.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Clean.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Clean.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Clean.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Clean.cs index 194d4221..76081a8d 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Clean.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Clean.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -27,7 +27,7 @@ public Task Clean( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET Clean Command /// @@ -49,7 +49,7 @@ public Task Clean( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET Clean Command /// @@ -66,12 +66,12 @@ public Task Clean( /// Cancellation token /// public Task Clean( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, CleanOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -87,12 +87,12 @@ public Task Clean( $"{string.Join(" ", projectOrSolutionOrFile)}", (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Clean( System.String projectOrSolutionOrFile, CleanOptions? options = null, @@ -104,14 +104,14 @@ public Task Clean( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Clean( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, CleanOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default @@ -121,12 +121,12 @@ public Task Clean( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } [PublicAPI] @@ -140,7 +140,7 @@ public System.String? ArtifactsPath get; init; } - + /// /// The configuration to clean for. The default for most projects is 'Debug'. /// @@ -149,7 +149,7 @@ public System.String? Configuration get; init; } - + /// /// Force the command to ignore any persistent build servers. /// @@ -158,7 +158,7 @@ public System.Boolean? DisableBuildServers get; init; } - + /// /// The target framework to clean for. The target framework must also be specified in the project file. /// @@ -167,31 +167,31 @@ public System.String? Framework get; init; } - + public System.String[]? GetItem { get; init; } - + public System.String[]? GetProperty { get; init; } - + public System.String[]? GetResultOutputFile { get; init; } - + public System.String[]? GetTargetResult { get; init; } - + /// /// Allows the command to stop and wait for user input or action (for example to complete authentication). /// @@ -200,7 +200,7 @@ public System.Boolean? Interactive get; init; } - + /// /// Do not display the startup banner or the copyright message. /// @@ -209,7 +209,7 @@ public System.Boolean? Nologo get; init; } - + /// /// The directory containing the build artifacts to clean. /// @@ -218,7 +218,7 @@ public System.String? Output get; init; } - + /// /// The target runtime to clean for. /// @@ -227,7 +227,7 @@ public System.String? Runtime get; init; } - + /// /// Build these targets in this project. Use a semicolon or a comma to separate multiple targets, or specify each target separately. /// @@ -236,7 +236,7 @@ public System.String[]? Target get; init; } - + /// /// Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]. /// @@ -245,7 +245,7 @@ public VerbosityOptions? Verbosity get; init; } - + public override string ToString() { return string.Join(' ', new[] @@ -295,6 +295,6 @@ Verbosity is not null } .Where(x => x is { Length: > 0 })); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/CleanFileBasedApps.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/CleanFileBasedApps.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/CleanFileBasedApps.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/CleanFileBasedApps.cs index e50c2cfd..3e67dc28 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/CleanFileBasedApps.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/CleanFileBasedApps.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Complete.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Complete.cs similarity index 92% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Complete.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Complete.cs index 0e2e8ed7..103d681d 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Complete.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Complete.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -12,14 +12,14 @@ public Task Complete( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + public Task Complete( - DecSm.Atom.Paths.RootedPath path, + Invex.FileSystem.RootedPath path, CompleteOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -35,14 +35,14 @@ public Task Complete( path, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Complete( - DecSm.Atom.Paths.RootedPath path, + Invex.FileSystem.RootedPath path, CompleteOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default @@ -52,12 +52,12 @@ public Task Complete( path, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } [PublicAPI] @@ -68,7 +68,7 @@ public System.Int32? Position get; init; } - + public override string ToString() { return string.Join(' ', new[] @@ -79,6 +79,6 @@ Position is not null } .Where(x => x is { Length: > 0 })); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Completions.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Completions.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Completions.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Completions.cs index b367b917..1a89a1b7 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Completions.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Completions.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Dnx.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Dnx.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Dnx.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Dnx.cs index a799db3a..0b679b04 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Dnx.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Dnx.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Format.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Format.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Format.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Format.cs index ea560906..d1900ffc 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Format.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Format.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Fsi.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Fsi.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Fsi.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Fsi.cs index b83d108e..bc0a7692 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Fsi.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Fsi.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/InternalReportinstallsuccess.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/InternalReportinstallsuccess.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/InternalReportinstallsuccess.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/InternalReportinstallsuccess.cs index 8d2456a1..03aca1c8 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/InternalReportinstallsuccess.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/InternalReportinstallsuccess.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/List.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/List.cs similarity index 93% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/List.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/List.cs index 1c4c23f3..283d88bd 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/List.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/List.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -23,7 +23,7 @@ public Task List( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// List references or packages of a .NET project. /// @@ -37,11 +37,11 @@ public Task List( /// Cancellation token /// public Task List( - DecSm.Atom.Paths.RootedPath projectOrSolution, + Invex.FileSystem.RootedPath projectOrSolution, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -55,14 +55,14 @@ public Task List( "list", projectOrSolution, }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task List( - DecSm.Atom.Paths.RootedPath projectOrSolution, + Invex.FileSystem.RootedPath projectOrSolution, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ) { @@ -70,11 +70,11 @@ public Task List( "list", projectOrSolution, }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ListPackage.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ListPackage.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ListPackage.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ListPackage.cs index 6fe4e167..98cf88b1 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ListPackage.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ListPackage.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ListReference.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ListReference.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ListReference.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ListReference.cs index 0300baad..09ec362f 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ListReference.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ListReference.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Msbuild.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Msbuild.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Msbuild.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Msbuild.cs index a19eb729..42bf9cd5 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Msbuild.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Msbuild.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/New.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/New.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/New.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/New.cs index df80be83..c014eb64 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/New.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/New.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewAlias.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewAlias.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NewAlias.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NewAlias.cs index c4e6122e..d9bde08d 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewAlias.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewAlias.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewAliasAdd.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewAliasAdd.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NewAliasAdd.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NewAliasAdd.cs index a6ae7ad3..cc7f772f 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewAliasAdd.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewAliasAdd.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewAliasShow.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewAliasShow.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NewAliasShow.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NewAliasShow.cs index 3e275003..087ecd88 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewAliasShow.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewAliasShow.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewCreate.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewCreate.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NewCreate.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NewCreate.cs index dd833386..00ab76dc 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewCreate.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewCreate.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewDetails.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewDetails.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NewDetails.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NewDetails.cs index ffa57cb2..9f31e991 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewDetails.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewDetails.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewInstall.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewInstall.cs similarity index 95% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NewInstall.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NewInstall.cs index b0258549..2ed31005 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewInstall.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewInstall.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -10,9 +10,9 @@ public partial interface IDotnetCli /// Installs a template package. /// /// - /// NuGet package ID or path to folder or NuGet package to install. + /// NuGet package ID or path to folder or NuGet package to install. /// To install the NuGet package of certain version, use <package ID>::<version>. - /// + /// /// /// /// Options @@ -29,14 +29,14 @@ public Task NewInstall( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// Installs a template package. /// /// - /// NuGet package ID or path to folder or NuGet package to install. + /// NuGet package ID or path to folder or NuGet package to install. /// To install the NuGet package of certain version, use <package ID>::<version>. - /// + /// /// /// /// Options @@ -53,14 +53,14 @@ public Task NewInstall( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// Installs a template package. /// /// - /// NuGet package ID or path to folder or NuGet package to install. + /// NuGet package ID or path to folder or NuGet package to install. /// To install the NuGet package of certain version, use <package ID>::<version>. - /// + /// /// /// /// Options @@ -72,12 +72,12 @@ public Task NewInstall( /// Cancellation token /// public Task NewInstall( - DecSm.Atom.Paths.RootedPath package, + Invex.FileSystem.RootedPath package, NewInstallOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -93,12 +93,12 @@ public Task NewInstall( $"{string.Join(" ", package)}", (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task NewInstall( System.String package, NewInstallOptions? options = null, @@ -110,14 +110,14 @@ public Task NewInstall( package, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task NewInstall( - DecSm.Atom.Paths.RootedPath package, + Invex.FileSystem.RootedPath package, NewInstallOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default @@ -127,12 +127,12 @@ public Task NewInstall( package, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } [PublicAPI] @@ -146,7 +146,7 @@ public System.String[]? AddSource get; init; } - + /// /// Allows installing template packages from the specified sources even if they would override a template package from another source. /// @@ -155,7 +155,7 @@ public System.Boolean? Force get; init; } - + /// /// Allows the command to stop and wait for user input or action (for example to complete authentication). /// @@ -164,7 +164,7 @@ public System.Boolean? Interactive get; init; } - + public override string ToString() { return string.Join(' ', new[] @@ -181,6 +181,6 @@ Interactive is true } .Where(x => x is { Length: > 0 })); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewList.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewList.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NewList.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NewList.cs index cea613cf..46433aa6 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewList.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewList.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewSearch.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewSearch.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NewSearch.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NewSearch.cs index 535e4528..446c4550 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewSearch.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewSearch.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewShowAlias.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewShowAlias.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NewShowAlias.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NewShowAlias.cs index 323a00fa..bc8bf68b 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewShowAlias.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewShowAlias.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewUninstall.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewUninstall.cs similarity index 94% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NewUninstall.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NewUninstall.cs index ab4b9fd6..196fbaaf 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewUninstall.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewUninstall.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -10,7 +10,7 @@ public partial interface IDotnetCli /// Uninstalls a template package. /// /// - /// NuGet package ID (without version) or path to folder to uninstall. + /// NuGet package ID (without version) or path to folder to uninstall. /// If command is specified without the argument, it lists all the template packages installed. /// /// @@ -24,12 +24,12 @@ public Task NewUninstall( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// Uninstalls a template package. /// /// - /// NuGet package ID (without version) or path to folder to uninstall. + /// NuGet package ID (without version) or path to folder to uninstall. /// If command is specified without the argument, it lists all the template packages installed. /// /// @@ -43,12 +43,12 @@ public Task NewUninstall( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// Uninstalls a template package. /// /// - /// NuGet package ID (without version) or path to folder to uninstall. + /// NuGet package ID (without version) or path to folder to uninstall. /// If command is specified without the argument, it lists all the template packages installed. /// /// @@ -58,11 +58,11 @@ public Task NewUninstall( /// Cancellation token /// public Task NewUninstall( - DecSm.Atom.Paths.RootedPath package, + Invex.FileSystem.RootedPath package, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -76,12 +76,12 @@ public Task NewUninstall( "new uninstall", $"{string.Join(" ", package)}", }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task NewUninstall( System.String package, ProcessRunOptions? processRunOptions = null, @@ -91,14 +91,14 @@ public Task NewUninstall( "new uninstall", package, }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task NewUninstall( - DecSm.Atom.Paths.RootedPath package, + Invex.FileSystem.RootedPath package, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ) { @@ -106,11 +106,11 @@ public Task NewUninstall( "new uninstall", package, }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewUpdate.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewUpdate.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NewUpdate.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NewUpdate.cs index 84a3c766..914f8bdf 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewUpdate.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewUpdate.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewUpdateApply.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewUpdateApply.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NewUpdateApply.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NewUpdateApply.cs index d83a32ce..1efc117f 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewUpdateApply.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewUpdateApply.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewUpdateCheck.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewUpdateCheck.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NewUpdateCheck.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NewUpdateCheck.cs index 278f985b..c7751f5c 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NewUpdateCheck.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NewUpdateCheck.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Nuget.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Nuget.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Nuget.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Nuget.cs index 72450407..f976e997 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Nuget.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Nuget.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetDelete.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetDelete.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetDelete.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetDelete.cs index a804abad..d8f5aca8 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetDelete.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetDelete.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetLocals.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetLocals.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetLocals.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetLocals.cs index bd580889..51609a6a 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetLocals.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetLocals.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetPush.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetPush.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetPush.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetPush.cs index 007c188a..dee5e609 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetPush.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetPush.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetSign.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetSign.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetSign.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetSign.cs index f64bf769..99c6bbbd 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetSign.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetSign.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrust.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrust.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrust.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrust.cs index 1063f2ca..c6c8ca8c 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrust.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrust.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustAuthor.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustAuthor.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustAuthor.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustAuthor.cs index d3f431d4..3536ecc7 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustAuthor.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustAuthor.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustCertificate.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustCertificate.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustCertificate.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustCertificate.cs index 5177198b..847457bc 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustCertificate.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustCertificate.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustList.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustList.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustList.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustList.cs index 22e0e7ff..f8e52004 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustList.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustList.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustRemove.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustRemove.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustRemove.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustRemove.cs index 1f935145..39dc3444 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustRemove.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustRemove.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustRepository.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustRepository.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustRepository.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustRepository.cs index aec298bf..0128a258 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustRepository.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustRepository.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustSource.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustSource.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustSource.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustSource.cs index 311ee171..c652d422 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustSource.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustSource.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustSync.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustSync.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustSync.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustSync.cs index a6548543..9af55911 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetTrustSync.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetTrustSync.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetVerify.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetVerify.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetVerify.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetVerify.cs index e6fef131..1d7c0b0f 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetVerify.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetVerify.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetWhy.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetWhy.cs similarity index 94% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/NugetWhy.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetWhy.cs index 362a528b..2e4af6d6 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/NugetWhy.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/NugetWhy.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -31,7 +31,7 @@ public Task NugetWhy( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// Shows the dependency graph for a particular package for a given project or solution. /// @@ -48,12 +48,12 @@ public Task NugetWhy( /// Cancellation token /// public Task NugetWhy( - DecSm.Atom.Paths.RootedPath projectorsolution, + Invex.FileSystem.RootedPath projectorsolution, NugetWhyOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -71,14 +71,14 @@ public Task NugetWhy( package, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task NugetWhy( - DecSm.Atom.Paths.RootedPath projectorsolution, + Invex.FileSystem.RootedPath projectorsolution, NugetWhyOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default @@ -88,12 +88,12 @@ public Task NugetWhy( projectorsolution, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } [PublicAPI] @@ -107,7 +107,7 @@ public System.Collections.Generic.List? Framework get; init; } - + public override string ToString() { return string.Join(' ', new[] @@ -118,6 +118,6 @@ Framework is not null } .Where(x => x is { Length: > 0 })); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Pack.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Pack.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Pack.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Pack.cs index 46bc8320..13efddfa 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Pack.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Pack.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -27,7 +27,7 @@ public Task Pack( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET Core NuGet Package Packer /// @@ -49,7 +49,7 @@ public Task Pack( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET Core NuGet Package Packer /// @@ -66,12 +66,12 @@ public Task Pack( /// Cancellation token /// public Task Pack( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, PackOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -87,12 +87,12 @@ public Task Pack( $"{string.Join(" ", projectOrSolutionOrFile)}", (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Pack( System.String projectOrSolutionOrFile, PackOptions? options = null, @@ -104,14 +104,14 @@ public Task Pack( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Pack( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, PackOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default @@ -121,12 +121,12 @@ public Task Pack( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } [PublicAPI] @@ -140,16 +140,16 @@ public System.String? ArtifactsPath get; init; } - + /// - /// + /// /// public System.String? Configfile { get; init; } - + /// /// The configuration to use for building the package. The default is 'Release'. /// @@ -158,7 +158,7 @@ public System.String? Configuration get; init; } - + /// /// Force the command to ignore any persistent build servers. /// @@ -167,16 +167,16 @@ public System.Boolean? DisableBuildServers get; init; } - + /// - /// + /// /// public System.Boolean? DisableParallel { get; init; } - + /// /// Force all dependencies to be resolved even if the last restore was successful. /// This is equivalent to deleting project.assets.json. @@ -186,40 +186,40 @@ public System.Boolean? Force get; init; } - + public System.String[]? GetItem { get; init; } - + public System.String[]? GetProperty { get; init; } - + public System.String[]? GetResultOutputFile { get; init; } - + public System.String[]? GetTargetResult { get; init; } - + /// - /// + /// /// public System.Boolean? IgnoreFailedSources { get; init; } - + /// /// Include PDBs and source files. Source files go into the 'src' folder in the resulting nuget package. /// @@ -228,7 +228,7 @@ public System.Boolean? IncludeSource get; init; } - + /// /// Include packages with symbols in addition to regular packages in output directory. /// @@ -237,7 +237,7 @@ public System.Boolean? IncludeSymbols get; init; } - + /// /// Allows the command to stop and wait for user input or action (for example to complete authentication). /// @@ -246,7 +246,7 @@ public System.Boolean? Interactive get; init; } - + /// /// Do not build the project before packing. Implies --no-restore. /// @@ -255,16 +255,16 @@ public System.Boolean? NoBuild get; init; } - + /// - /// + /// /// public System.Boolean? NoCache { get; init; } - + /// /// Do not restore project-to-project references and only restore the specified project. /// @@ -273,16 +273,16 @@ public System.Boolean? NoDependencies get; init; } - + /// - /// + /// /// public System.Boolean? NoHttpCache { get; init; } - + /// /// Do not restore the project before building. /// @@ -291,7 +291,7 @@ public System.Boolean? NoRestore get; init; } - + /// /// Do not display the startup banner or the copyright message. /// @@ -300,7 +300,7 @@ public System.Boolean? Nologo get; init; } - + /// /// The output directory to place built packages in. /// @@ -309,28 +309,28 @@ public System.String? Output get; init; } - + /// - /// + /// /// public System.String? Packages { get; init; } - + public System.Collections.Generic.IReadOnlyDictionary? Property { get; init; } - + public System.Collections.Generic.IReadOnlyDictionary? RestoreProperty { get; init; } - + /// /// The target runtime to build for. /// @@ -339,7 +339,7 @@ public System.String? Runtime get; init; } - + /// /// Set the serviceable flag in the package. See https://aka.ms/nupkgservicing for more information. /// @@ -348,16 +348,16 @@ public System.Boolean? Serviceable get; init; } - + /// - /// + /// /// public System.Collections.Generic.IEnumerable? Source { get; init; } - + /// /// Build these targets in this project. Use a semicolon or a comma to separate multiple targets, or specify each target separately. /// @@ -366,7 +366,7 @@ public System.String[]? Target get; init; } - + /// /// Use current runtime as the target runtime. /// @@ -375,7 +375,7 @@ public System.Boolean? UseCurrentRuntime get; init; } - + /// /// Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]. /// @@ -384,7 +384,7 @@ public VerbosityOptions? Verbosity get; init; } - + /// /// The version of the package to create /// @@ -393,7 +393,7 @@ public string? Version get; init; } - + /// /// Set the value of the $(VersionSuffix) property to use when building the project. /// @@ -402,7 +402,7 @@ public System.String? VersionSuffix get; init; } - + public override string ToString() { return string.Join(' ', new[] @@ -506,6 +506,6 @@ VersionSuffix is not null } .Where(x => x is { Length: > 0 })); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Package.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Package.cs similarity index 93% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Package.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Package.cs index 42c1b05b..568121ad 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Package.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Package.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/PackageAdd.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageAdd.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/PackageAdd.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageAdd.cs index d1c93755..a74b4cbc 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/PackageAdd.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageAdd.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/PackageList.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageList.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/PackageList.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageList.cs index 6f906a1a..9a5450c2 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/PackageList.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageList.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/PackageRemove.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageRemove.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/PackageRemove.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageRemove.cs index 7b7e1115..21fedb07 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/PackageRemove.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageRemove.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/PackageSearch.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageSearch.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/PackageSearch.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageSearch.cs index cb59f6aa..d4b084ae 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/PackageSearch.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageSearch.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/PackageUpdate.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageUpdate.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/PackageUpdate.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageUpdate.cs index d647d235..1f6474ad 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/PackageUpdate.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/PackageUpdate.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Parse.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Parse.cs similarity index 93% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Parse.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Parse.cs index fbdb2696..a96a5b87 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Parse.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Parse.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Project.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Project.cs similarity index 93% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Project.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Project.cs index a387a6f5..1a762132 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Project.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Project.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ProjectConvert.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ProjectConvert.cs similarity index 95% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ProjectConvert.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ProjectConvert.cs index 896a3bdd..c4bb01a8 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ProjectConvert.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ProjectConvert.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -27,7 +27,7 @@ public Task ProjectConvert( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// Convert a file-based program to a project-based program. /// @@ -44,12 +44,12 @@ public Task ProjectConvert( /// Cancellation token /// public Task ProjectConvert( - DecSm.Atom.Paths.RootedPath file, + Invex.FileSystem.RootedPath file, ProjectConvertOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -65,14 +65,14 @@ public Task ProjectConvert( file, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task ProjectConvert( - DecSm.Atom.Paths.RootedPath file, + Invex.FileSystem.RootedPath file, ProjectConvertOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default @@ -82,12 +82,12 @@ public Task ProjectConvert( file, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } [PublicAPI] @@ -101,7 +101,7 @@ public System.Boolean? DryRun get; init; } - + /// /// Force conversion even if there are malformed directives. /// @@ -110,7 +110,7 @@ public System.Boolean? Force get; init; } - + /// /// Allows the command to stop and wait for user input or action (for example to complete authentication). /// @@ -119,7 +119,7 @@ public System.Boolean? Interactive get; init; } - + /// /// Location to place the generated output. /// @@ -128,7 +128,7 @@ public System.IO.FileInfo? Output get; init; } - + public override string ToString() { return string.Join(' ', new[] @@ -148,6 +148,6 @@ Output is not null } .Where(x => x is { Length: > 0 })); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Publish.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Publish.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Publish.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Publish.cs index f12e097c..32635a4c 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Publish.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Publish.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -27,7 +27,7 @@ public Task Publish( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// Publisher for the .NET Platform /// @@ -49,7 +49,7 @@ public Task Publish( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// Publisher for the .NET Platform /// @@ -66,12 +66,12 @@ public Task Publish( /// Cancellation token /// public Task Publish( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, PublishOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -87,12 +87,12 @@ public Task Publish( $"{string.Join(" ", projectOrSolutionOrFile)}", (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Publish( System.String projectOrSolutionOrFile, PublishOptions? options = null, @@ -104,14 +104,14 @@ public Task Publish( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Publish( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, PublishOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default @@ -121,12 +121,12 @@ public Task Publish( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } [PublicAPI] @@ -140,7 +140,7 @@ public System.String? Arch get; init; } - + /// /// The artifacts path. All output from the project, including build, publish, and pack output, will go in subfolders under the specified path. /// @@ -149,16 +149,16 @@ public System.String? ArtifactsPath get; init; } - + /// - /// + /// /// public System.String? Configfile { get; init; } - + /// /// The configuration to publish for. The default is 'Release' for NET 8.0 projects and above, but 'Debug' for older projects. /// @@ -167,7 +167,7 @@ public System.String? Configuration get; init; } - + /// /// Force the command to ignore any persistent build servers. /// @@ -176,16 +176,16 @@ public System.Boolean? DisableBuildServers get; init; } - + /// - /// + /// /// public System.Boolean? DisableParallel { get; init; } - + /// /// Force all dependencies to be resolved even if the last restore was successful. /// This is equivalent to deleting project.assets.json. @@ -195,7 +195,7 @@ public System.Boolean? Force get; init; } - + /// /// The target framework to publish for. The target framework has to be specified in the project file. /// @@ -204,40 +204,40 @@ public System.String? Framework get; init; } - + public System.String[]? GetItem { get; init; } - + public System.String[]? GetProperty { get; init; } - + public System.String[]? GetResultOutputFile { get; init; } - + public System.String[]? GetTargetResult { get; init; } - + /// - /// + /// /// public System.Boolean? IgnoreFailedSources { get; init; } - + /// /// Allows the command to stop and wait for user input or action (for example to complete authentication). /// @@ -246,7 +246,7 @@ public System.Boolean? Interactive get; init; } - + /// /// The path to a target manifest file that contains the list of packages to be excluded from the publish step. /// @@ -255,7 +255,7 @@ public System.Collections.Generic.IEnumerable? Manifest get; init; } - + /// /// Do not build the project before publishing. Implies --no-restore. /// @@ -264,16 +264,16 @@ public System.Boolean? NoBuild get; init; } - + /// - /// + /// /// public System.Boolean? NoCache { get; init; } - + /// /// Do not restore project-to-project references and only restore the specified project. /// @@ -282,16 +282,16 @@ public System.Boolean? NoDependencies get; init; } - + /// - /// + /// /// public System.Boolean? NoHttpCache { get; init; } - + /// /// Do not restore the project before building. /// @@ -300,7 +300,7 @@ public System.Boolean? NoRestore get; init; } - + /// /// Publish your application as a framework dependent application. A compatible .NET runtime must be installed on the target machine to run your application. /// @@ -309,7 +309,7 @@ public System.Boolean? NoSelfContained get; init; } - + /// /// Do not display the startup banner or the copyright message. /// @@ -318,7 +318,7 @@ public System.Boolean? Nologo get; init; } - + /// /// The target operating system. /// @@ -327,7 +327,7 @@ public System.String? Os get; init; } - + /// /// The output directory to place the published artifacts in. /// @@ -336,28 +336,28 @@ public System.String? Output get; init; } - + /// - /// + /// /// public System.String? Packages { get; init; } - + public System.Collections.Generic.IReadOnlyDictionary? Property { get; init; } - + public System.Collections.Generic.IReadOnlyDictionary? RestoreProperty { get; init; } - + /// /// The target runtime to publish for. This is used when creating a self-contained deployment. /// The default is to publish a framework-dependent application. @@ -367,7 +367,7 @@ public System.String? Runtime get; init; } - + /// /// Publish the .NET runtime with your application so the runtime doesn't need to be installed on the target machine. /// The default is 'false.' However, when targeting .NET 7 or lower, the default is 'true' if a runtime identifier is specified. @@ -377,16 +377,16 @@ public System.Boolean? SelfContained get; init; } - + /// - /// + /// /// public System.Collections.Generic.IEnumerable? Source { get; init; } - + /// /// Build these targets in this project. Use a semicolon or a comma to separate multiple targets, or specify each target separately. /// @@ -395,7 +395,7 @@ public System.String[]? Target get; init; } - + /// /// Use current runtime as the target runtime. /// @@ -404,7 +404,7 @@ public System.Boolean? UseCurrentRuntime get; init; } - + /// /// Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]. /// @@ -413,7 +413,7 @@ public VerbosityOptions? Verbosity get; init; } - + /// /// Set the value of the $(VersionSuffix) property to use when building the project. /// @@ -422,7 +422,7 @@ public System.String? VersionSuffix get; init; } - + public override string ToString() { return string.Join(' ', new[] @@ -532,6 +532,6 @@ VersionSuffix is not null } .Where(x => x is { Length: > 0 })); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Reference.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Reference.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Reference.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Reference.cs index 546c9dd9..9f9720fc 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Reference.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Reference.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ReferenceAdd.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ReferenceAdd.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ReferenceAdd.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ReferenceAdd.cs index fc25305c..62a0191d 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ReferenceAdd.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ReferenceAdd.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ReferenceList.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ReferenceList.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ReferenceList.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ReferenceList.cs index 56412fe2..738ee9de 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ReferenceList.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ReferenceList.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ReferenceRemove.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ReferenceRemove.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ReferenceRemove.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ReferenceRemove.cs index 0c575228..d3e21ee1 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ReferenceRemove.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ReferenceRemove.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Remove.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Remove.cs similarity index 93% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Remove.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Remove.cs index 09cd5413..b7d59c39 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Remove.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Remove.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -23,7 +23,7 @@ public Task Remove( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET Remove Command /// @@ -37,11 +37,11 @@ public Task Remove( /// Cancellation token /// public Task Remove( - DecSm.Atom.Paths.RootedPath projectOrFile, + Invex.FileSystem.RootedPath projectOrFile, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -55,14 +55,14 @@ public Task Remove( "remove", projectOrFile, }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Remove( - DecSm.Atom.Paths.RootedPath projectOrFile, + Invex.FileSystem.RootedPath projectOrFile, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ) { @@ -70,11 +70,11 @@ public Task Remove( "remove", projectOrFile, }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/RemovePackage.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/RemovePackage.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/RemovePackage.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/RemovePackage.cs index 4fa8d3fb..bc57221c 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/RemovePackage.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/RemovePackage.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/RemoveReference.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/RemoveReference.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/RemoveReference.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/RemoveReference.cs index 6c65519f..fee90386 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/RemoveReference.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/RemoveReference.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Restore.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Restore.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Restore.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Restore.cs index 60d62b2a..76bd4964 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Restore.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Restore.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -27,7 +27,7 @@ public Task Restore( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET dependency restorer /// @@ -49,7 +49,7 @@ public Task Restore( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET dependency restorer /// @@ -66,12 +66,12 @@ public Task Restore( /// Cancellation token /// public Task Restore( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, RestoreOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -87,12 +87,12 @@ public Task Restore( $"{string.Join(" ", projectOrSolutionOrFile)}", (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Restore( System.String projectOrSolutionOrFile, RestoreOptions? options = null, @@ -104,14 +104,14 @@ public Task Restore( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Restore( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, RestoreOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default @@ -121,12 +121,12 @@ public Task Restore( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } [PublicAPI] @@ -140,7 +140,7 @@ public System.String? Arch get; init; } - + /// /// The artifacts path. All output from the project, including build, publish, and pack output, will go in subfolders under the specified path. /// @@ -149,7 +149,7 @@ public System.String? ArtifactsPath get; init; } - + /// /// The NuGet configuration file to use. /// @@ -158,7 +158,7 @@ public System.String? Configfile get; init; } - + /// /// Force the command to ignore any persistent build servers. /// @@ -167,7 +167,7 @@ public System.Boolean? DisableBuildServers get; init; } - + /// /// Prevent restoring multiple projects in parallel. /// @@ -176,7 +176,7 @@ public System.Boolean? DisableParallel get; init; } - + /// /// Force all dependencies to be resolved even if the last restore was successful. /// This is equivalent to deleting project.assets.json. @@ -186,7 +186,7 @@ public System.Boolean? Force get; init; } - + /// /// Forces restore to reevaluate all dependencies even if a lock file already exists. /// @@ -195,31 +195,31 @@ public System.Boolean? ForceEvaluate get; init; } - + public System.String[]? GetItem { get; init; } - + public System.String[]? GetProperty { get; init; } - + public System.String[]? GetResultOutputFile { get; init; } - + public System.String[]? GetTargetResult { get; init; } - + /// /// Treat package source failures as warnings. /// @@ -228,7 +228,7 @@ public System.Boolean? IgnoreFailedSources get; init; } - + /// /// Allows the command to stop and wait for user input or action (for example to complete authentication). /// @@ -237,7 +237,7 @@ public System.Boolean? Interactive get; init; } - + /// /// Output location where project lock file is written. By default, this is 'PROJECT_ROOT\packages.lock.json'. /// @@ -246,7 +246,7 @@ public System.String? LockFilePath get; init; } - + /// /// Don't allow updating project lock file. /// @@ -255,16 +255,16 @@ public System.Boolean? LockedMode get; init; } - + /// - /// + /// /// public System.Boolean? NoCache { get; init; } - + /// /// Do not restore project-to-project references and only restore the specified project. /// @@ -273,7 +273,7 @@ public System.Boolean? NoDependencies get; init; } - + /// /// Disable Http Caching for packages. /// @@ -282,7 +282,7 @@ public System.Boolean? NoHttpCache get; init; } - + /// /// The target operating system. /// @@ -291,7 +291,7 @@ public System.String? Os get; init; } - + /// /// The directory to restore packages to. /// @@ -300,19 +300,19 @@ public System.String? Packages get; init; } - + public System.Collections.Generic.IReadOnlyDictionary? Property { get; init; } - + public System.Collections.Generic.IReadOnlyDictionary? RestoreProperty { get; init; } - + /// /// The target runtime to restore packages for. /// @@ -321,7 +321,7 @@ public System.Collections.Generic.IEnumerable? Runtime get; init; } - + /// /// The NuGet package source to use for the restore. /// @@ -330,7 +330,7 @@ public System.Collections.Generic.IEnumerable? Source get; init; } - + /// /// Build these targets in this project. Use a semicolon or a comma to separate multiple targets, or specify each target separately. /// @@ -339,7 +339,7 @@ public System.String[]? Target get; init; } - + /// /// Use current runtime as the target runtime. /// @@ -348,7 +348,7 @@ public System.Boolean? UseCurrentRuntime get; init; } - + /// /// Enables project lock file to be generated and used with restore. /// @@ -357,7 +357,7 @@ public System.Boolean? UseLockFile get; init; } - + /// /// Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]. /// @@ -366,7 +366,7 @@ public VerbosityOptions? Verbosity get; init; } - + public override string ToString() { return string.Join(' ', new[] @@ -458,6 +458,6 @@ Verbosity is not null } .Where(x => x is { Length: > 0 })); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Run.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Run.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Run.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Run.cs index 19db942f..32200a6d 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Run.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Run.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -31,7 +31,7 @@ public Task Run( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET Run Command /// @@ -53,7 +53,7 @@ public Task Run( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET Run Command /// @@ -70,12 +70,12 @@ public Task Run( /// Cancellation token /// public Task Run( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, RunOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -93,12 +93,12 @@ public Task Run( $"{string.Join(" ", applicationArguments)}", (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Run( System.String projectOrSolutionOrFile, RunOptions? options = null, @@ -110,14 +110,14 @@ public Task Run( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Run( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, RunOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default @@ -127,12 +127,12 @@ public Task Run( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } [PublicAPI] @@ -146,7 +146,7 @@ public System.String? Arch get; init; } - + /// /// The artifacts path. All output from the project, including build, publish, and pack output, will go in subfolders under the specified path. /// @@ -155,7 +155,7 @@ public System.String? ArtifactsPath get; init; } - + /// /// The configuration to run for. The default for most projects is 'Debug'. /// @@ -164,7 +164,7 @@ public System.String? Configuration get; init; } - + /// /// Force the command to ignore any persistent build servers. /// @@ -173,25 +173,25 @@ public System.Boolean? DisableBuildServers get; init; } - + /// - /// Sets the value of an environment variable. - /// Creates the variable if it does not exist, overrides if it does. + /// Sets the value of an environment variable. + /// Creates the variable if it does not exist, overrides if it does. /// This argument can be specified multiple times to provide multiple variables. - /// + /// /// Examples: /// -e VARIABLE=abc /// -e VARIABLE="value with spaces" /// -e VARIABLE="value;seperated with;semicolons" /// -e VAR1=abc -e VAR2=def -e VAR3=ghi - /// + /// /// public System.Collections.Generic.IReadOnlyDictionary? Environment { get; init; } - + /// /// The path to the file-based app to run (can be also passed as the first argument if there is no project in the current directory). /// @@ -200,7 +200,7 @@ public System.String? File get; init; } - + /// /// The target framework to run for. The target framework must also be specified in the project file. /// @@ -209,7 +209,7 @@ public System.String? Framework get; init; } - + /// /// Allows the command to stop and wait for user input or action (for example to complete authentication). /// @@ -218,7 +218,7 @@ public System.Boolean? Interactive get; init; } - + /// /// The name of the launch profile (if any) to use when launching the application. /// @@ -227,7 +227,7 @@ public System.String? LaunchProfile get; init; } - + /// /// Do not build the project before running. Implies --no-restore. /// @@ -236,7 +236,7 @@ public System.Boolean? NoBuild get; init; } - + /// /// Skip up to date checks and always build the program before running. /// @@ -245,7 +245,7 @@ public System.Boolean? NoCache get; init; } - + /// /// Do not attempt to use launchSettings.json or [app].run.json to configure the application. /// @@ -254,7 +254,7 @@ public System.Boolean? NoLaunchProfile get; init; } - + /// /// Do not restore the project before building. /// @@ -263,7 +263,7 @@ public System.Boolean? NoRestore get; init; } - + /// /// Publish your application as a framework dependent application. A compatible .NET runtime must be installed on the target machine to run your application. /// @@ -272,7 +272,7 @@ public System.Boolean? NoSelfContained get; init; } - + /// /// The target operating system. /// @@ -281,7 +281,7 @@ public System.String? Os get; init; } - + /// /// The path to the project file to run (defaults to the current directory if there is only one project). /// @@ -290,13 +290,13 @@ public System.String? Project get; init; } - + public System.Collections.Generic.IReadOnlyDictionary? Property { get; init; } - + /// /// The target runtime to run for. /// @@ -305,7 +305,7 @@ public System.String? Runtime get; init; } - + /// /// Publish the .NET runtime with your application so the runtime doesn't need to be installed on the target machine. /// The default is 'false.' However, when targeting .NET 7 or lower, the default is 'true' if a runtime identifier is specified. @@ -315,7 +315,7 @@ public System.Boolean? SelfContained get; init; } - + /// /// Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]. /// @@ -324,7 +324,7 @@ public VerbosityOptions? Verbosity get; init; } - + public override string ToString() { return string.Join(' ', new[] @@ -392,6 +392,6 @@ Verbosity is not null } .Where(x => x is { Length: > 0 })); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/RunApi.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/RunApi.cs similarity index 93% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/RunApi.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/RunApi.cs index 901ccdd9..96954698 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/RunApi.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/RunApi.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Sdk.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Sdk.cs similarity index 95% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Sdk.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Sdk.cs index d2d84c58..d9e14ada 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Sdk.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Sdk.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/SdkCheck.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/SdkCheck.cs similarity index 95% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/SdkCheck.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/SdkCheck.cs index 4e26ea20..7ddf831a 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/SdkCheck.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/SdkCheck.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Solution.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Solution.cs similarity index 93% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Solution.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Solution.cs index 5f5e5c01..46e1b46f 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Solution.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Solution.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -23,7 +23,7 @@ public Task Solution( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET modify solution file command /// @@ -37,11 +37,11 @@ public Task Solution( /// Cancellation token /// public Task Solution( - DecSm.Atom.Paths.RootedPath slnFile, + Invex.FileSystem.RootedPath slnFile, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -55,14 +55,14 @@ public Task Solution( "solution", slnFile, }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Solution( - DecSm.Atom.Paths.RootedPath slnFile, + Invex.FileSystem.RootedPath slnFile, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ) { @@ -70,11 +70,11 @@ public Task Solution( "solution", slnFile, }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/SolutionAdd.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/SolutionAdd.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/SolutionAdd.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/SolutionAdd.cs index b167ebf2..36594725 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/SolutionAdd.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/SolutionAdd.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/SolutionList.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/SolutionList.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/SolutionList.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/SolutionList.cs index 3247bc24..a0566a03 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/SolutionList.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/SolutionList.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/SolutionMigrate.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/SolutionMigrate.cs similarity index 93% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/SolutionMigrate.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/SolutionMigrate.cs index 86ca6846..db2f8730 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/SolutionMigrate.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/SolutionMigrate.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -23,7 +23,7 @@ public Task SolutionMigrate( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// Generate a .slnx file from a .sln file. /// @@ -37,11 +37,11 @@ public Task SolutionMigrate( /// Cancellation token /// public Task SolutionMigrate( - DecSm.Atom.Paths.RootedPath slnFile, + Invex.FileSystem.RootedPath slnFile, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -55,14 +55,14 @@ public Task SolutionMigrate( "solution migrate", slnFile, }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task SolutionMigrate( - DecSm.Atom.Paths.RootedPath slnFile, + Invex.FileSystem.RootedPath slnFile, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ) { @@ -70,11 +70,11 @@ public Task SolutionMigrate( "solution migrate", slnFile, }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/SolutionRemove.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/SolutionRemove.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/SolutionRemove.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/SolutionRemove.cs index 67dd15e8..b6a3598e 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/SolutionRemove.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/SolutionRemove.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Store.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Store.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Store.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Store.cs index 4d23beeb..7df284ac 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Store.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Store.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Test.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Test.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Test.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Test.cs index 8bdb8ade..a424f0e9 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Test.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Test.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { @@ -27,7 +27,7 @@ public Task Test( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. /// @@ -49,7 +49,7 @@ public Task Test( ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + /// /// .NET Test Command for VSTest. To use Microsoft.Testing.Platform, opt-in to the Microsoft.Testing.Platform-based command via global.json. For more information, see https://aka.ms/dotnet-test. /// @@ -66,12 +66,12 @@ public Task Test( /// Cancellation token /// public Task Test( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, TestOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default ); - + } internal partial class DotnetCli @@ -87,12 +87,12 @@ public Task Test( $"{string.Join(" ", projectOrSolutionOrFile)}", (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Test( System.String projectOrSolutionOrFile, TestOptions? options = null, @@ -104,14 +104,14 @@ public Task Test( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + public Task Test( - DecSm.Atom.Paths.RootedPath projectOrSolutionOrFile, + Invex.FileSystem.RootedPath projectOrSolutionOrFile, TestOptions? options = null, ProcessRunOptions? processRunOptions = null, CancellationToken cancellationToken = default @@ -121,12 +121,12 @@ public Task Test( projectOrSolutionOrFile, (options ?? new()).ToString(), }.Where(x => x is { Length: > 0 })); - + return processRunner.RunAsync((processRunOptions is null ? new("dotnet", argsString) : processRunOptions with { Name = "dotnet", Args = argsString }), cancellationToken); } - + } [PublicAPI] @@ -140,7 +140,7 @@ public System.String? Arch get; init; } - + /// /// The artifacts path. All output from the project, including build, publish, and pack output, will go in subfolders under the specified path. /// @@ -149,14 +149,14 @@ public System.String? ArtifactsPath get; init; } - + /// /// Runs the tests in blame mode. This option is helpful in isolating problematic tests that cause the test host to crash or hang, but it does not create a memory dump by default. - /// + /// /// When a crash is detected, it creates an sequence file in TestResults/guid/guid_Sequence.xml that captures the order of tests that were run before the crash. - /// + /// /// Based on the additional settings, hang dump or crash dump can also be collected. - /// + /// /// Example: /// Timeout the test run when test takes more than the default timeout of 1 hour, and collect crash dump when the test host exits unexpectedly. /// (Crash dumps require additional setup, see below.) @@ -164,25 +164,25 @@ public System.String? ArtifactsPath /// Example: /// Timeout the test run when a test takes more than 20 minutes and collect hang dump. /// dotnet test --blame-hang-timeout 20min - /// + /// /// public System.Boolean? Blame { get; init; } - + /// /// Runs the tests in blame mode and collects a crash dump when the test host exits unexpectedly. This option depends on the version of .NET used, the type of error, and the operating system. - /// + /// /// For exceptions in managed code, a dump will be automatically collected on .NET 5.0 and later versions. It will generate a dump for testhost or any child process that also ran on .NET 5.0 and crashed. Crashes in native code will not generate a dump. This option works on Windows, macOS, and Linux. - /// + /// /// Crash dumps in native code, or when targetting .NET Framework, or .NET Core 3.1 and earlier versions, can only be collected on Windows, by using Procdump. A directory that contains procdump.exe and procdump64.exe must be in the PATH or PROCDUMP_PATH environment variable. - /// + /// /// The tools can be downloaded here: https://docs.microsoft.com/sysinternals/downloads/procdump - /// + /// /// To collect a crash dump from a native application running on .NET 5.0 or later, the usage of Procdump can be forced by setting the VSTEST_DUMP_FORCEPROCDUMP environment variable to 1. - /// + /// /// Implies --blame. /// public System.Boolean? BlameCrash @@ -190,7 +190,7 @@ public System.Boolean? BlameCrash get; init; } - + /// /// Enables collecting crash dump on expected as well as unexpected testhost exit. /// @@ -199,7 +199,7 @@ public System.Boolean? BlameCrashCollectAlways get; init; } - + /// /// The type of crash dump to be collected. Supported values are full (default) and mini. Implies --blame-crash. /// @@ -208,7 +208,7 @@ public System.String? BlameCrashDumpType get; init; } - + /// /// Run the tests in blame mode and enables collecting hang dump when test exceeds the given timeout. /// @@ -217,7 +217,7 @@ public System.Boolean? BlameHang get; init; } - + /// /// The type of crash dump to be collected. The supported values are full (default), mini, and none. When 'none' is used then test host is terminated on timeout, but no dump is collected. Implies --blame-hang. /// @@ -226,7 +226,7 @@ public System.String? BlameHangDumpType get; init; } - + /// /// Per-test timeout, after which hang dump is triggered and the testhost process is terminated. Default is 1h. /// The timeout value is specified in the following format: 1.5h / 90m / 5400s / 5400000ms. When no unit is used (e.g. 5400000), the value is assumed to be in milliseconds. @@ -238,7 +238,7 @@ public System.String? BlameHangTimeout get; init; } - + /// /// The friendly name of the data collector to use for the test run. /// More info here: https://aka.ms/vstest-collect @@ -248,7 +248,7 @@ public System.Collections.Generic.IEnumerable? Collect get; init; } - + /// /// The configuration to use for running tests. The default for most projects is 'Debug'. /// @@ -257,7 +257,7 @@ public System.String? Configuration get; init; } - + /// /// Enable verbose logging to the specified file. /// @@ -266,7 +266,7 @@ public System.String? Diag get; init; } - + /// /// Force the command to ignore any persistent build servers. /// @@ -275,26 +275,26 @@ public System.Boolean? DisableBuildServers get; init; } - + /// - /// Sets the value of an environment variable. - /// Creates the variable if it does not exist, overrides if it does. - /// This will force the tests to be run in an isolated process. + /// Sets the value of an environment variable. + /// Creates the variable if it does not exist, overrides if it does. + /// This will force the tests to be run in an isolated process. /// This argument can be specified multiple times to provide multiple variables. - /// + /// /// Examples: /// -e VARIABLE=abc /// -e VARIABLE="value with spaces" /// -e VARIABLE="value;seperated with;semicolons" /// -e VAR1=abc -e VAR2=def -e VAR3=ghi - /// + /// /// public System.Collections.Generic.IReadOnlyDictionary? Environment { get; init; } - + /// /// Run tests that match the given expression. /// Examples: @@ -302,14 +302,14 @@ public System.Boolean? DisableBuildServers /// Run a test with the specified full name: --filter "FullyQualifiedName=Namespace.ClassName.MethodName" /// Run tests that contain the specified name: --filter "FullyQualifiedName~Namespace.Class" /// See https://aka.ms/vstest-filtering for more information on filtering support. - /// + /// /// public System.String? Filter { get; init; } - + /// /// The target framework to run tests for. The target framework must also be specified in the project file. /// @@ -318,7 +318,7 @@ public System.String? Framework get; init; } - + /// /// Allows the command to stop and wait for user input or action (for example to complete authentication). /// @@ -327,7 +327,7 @@ public System.Boolean? Interactive get; init; } - + /// /// List the discovered tests instead of running the tests. /// @@ -336,7 +336,7 @@ public System.Boolean? ListTests get; init; } - + /// /// The logger to use for test results. /// Examples: @@ -349,7 +349,7 @@ public System.Collections.Generic.IEnumerable? Logger get; init; } - + /// /// Do not build the project before testing. Implies --no-restore. /// @@ -358,7 +358,7 @@ public System.Boolean? NoBuild get; init; } - + /// /// Do not restore the project before building. /// @@ -367,7 +367,7 @@ public System.Boolean? NoRestore get; init; } - + /// /// Run test(s), without displaying Microsoft Testplatform banner /// @@ -376,7 +376,7 @@ public System.Boolean? Nologo get; init; } - + /// /// The target operating system. /// @@ -385,7 +385,7 @@ public System.String? Os get; init; } - + /// /// The output directory to place built artifacts in. /// @@ -394,13 +394,13 @@ public System.String? Output get; init; } - + public System.Collections.Generic.IReadOnlyDictionary? Property { get; init; } - + /// /// The directory where the test results will be placed. /// The specified directory will be created if it does not exist. @@ -410,7 +410,7 @@ public System.String? ResultsDirectory get; init; } - + /// /// The target runtime to test for. /// @@ -419,7 +419,7 @@ public System.String? Runtime get; init; } - + /// /// The settings file to use when running tests. /// @@ -428,7 +428,7 @@ public System.String? Settings get; init; } - + /// /// Build these targets in this project. Use a semicolon or a comma to separate multiple targets, or specify each target separately. /// @@ -437,7 +437,7 @@ public System.String[]? Target get; init; } - + /// /// The path to the custom adapters to use for the test run. /// @@ -446,7 +446,7 @@ public System.Collections.Generic.IEnumerable? TestAdapterPath get; init; } - + /// /// Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]. /// @@ -455,7 +455,7 @@ public VerbosityOptions? Verbosity get; init; } - + public override string ToString() { return string.Join(' ', new[] @@ -556,6 +556,6 @@ Verbosity is not null } .Where(x => x is { Length: > 0 })); } - + } diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Tool.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Tool.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Tool.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Tool.cs index 29af09a8..496e9bbb 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Tool.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Tool.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolExecute.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolExecute.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ToolExecute.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolExecute.cs index 944ff056..9ef7411a 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolExecute.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolExecute.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolInstall.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolInstall.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ToolInstall.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolInstall.cs index f23b4d41..b4be393a 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolInstall.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolInstall.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolList.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolList.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ToolList.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolList.cs index b26d4d65..cbcae42d 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolList.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolList.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolRestore.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolRestore.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ToolRestore.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolRestore.cs index 87bf3526..a554c828 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolRestore.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolRestore.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolRun.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolRun.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ToolRun.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolRun.cs index 037a5fc8..179f6aaf 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolRun.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolRun.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolSearch.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolSearch.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ToolSearch.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolSearch.cs index b2981650..7a24fa10 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolSearch.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolSearch.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolUninstall.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolUninstall.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ToolUninstall.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolUninstall.cs index ad97926a..079c2016 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolUninstall.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolUninstall.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolUpdate.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolUpdate.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/ToolUpdate.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolUpdate.cs index d5ff45b9..638f5a55 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/ToolUpdate.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/ToolUpdate.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Vstest.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Vstest.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Vstest.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Vstest.cs index 8467414b..31e763fb 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Vstest.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Vstest.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/Workload.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Workload.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/Workload.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/Workload.cs index 8ddb2cc1..f38b23e3 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/Workload.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/Workload.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadClean.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadClean.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadClean.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadClean.cs index 1eeb4e41..c69ab736 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadClean.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadClean.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadConfig.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadConfig.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadConfig.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadConfig.cs index 824ad93b..8323c014 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadConfig.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadConfig.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadElevate.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadElevate.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadElevate.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadElevate.cs index 39ac47cc..2a92659b 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadElevate.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadElevate.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadHistory.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadHistory.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadHistory.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadHistory.cs index 1360081d..03b108d0 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadHistory.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadHistory.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadInstall.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadInstall.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadInstall.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadInstall.cs index dfafbec1..284dd75b 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadInstall.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadInstall.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadList.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadList.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadList.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadList.cs index 1b6a3f15..25f9ba2b 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadList.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadList.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadRepair.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadRepair.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadRepair.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadRepair.cs index dcd96fe6..db611f37 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadRepair.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadRepair.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadRestore.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadRestore.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadRestore.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadRestore.cs index 39e36fec..9373391f 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadRestore.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadRestore.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadSearch.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadSearch.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadSearch.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadSearch.cs index dbb3714e..ea19298b 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadSearch.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadSearch.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadSearchVersion.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadSearchVersion.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadSearchVersion.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadSearchVersion.cs index 1e6ffa23..c5dabfb3 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadSearchVersion.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadSearchVersion.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadUninstall.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadUninstall.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadUninstall.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadUninstall.cs index 81eb8fff..74e179a5 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadUninstall.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadUninstall.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadUpdate.cs b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadUpdate.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadUpdate.cs rename to src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadUpdate.cs index 62a33767..68bd24d2 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/Generated/WorkloadUpdate.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/Generated/WorkloadUpdate.cs @@ -2,7 +2,7 @@ #nullable enable -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; public partial interface IDotnetCli { diff --git a/DecSm.Atom.Module.Dotnet/Cli/IDotnetCli.cs b/src/Invex.Atom.Module.Dotnet/Cli/IDotnetCli.cs similarity index 95% rename from DecSm.Atom.Module.Dotnet/Cli/IDotnetCli.cs rename to src/Invex.Atom.Module.Dotnet/Cli/IDotnetCli.cs index 1183e6dd..92c4c9f0 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/IDotnetCli.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/IDotnetCli.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; /// /// Represents the base interface for interacting with the .NET CLI. diff --git a/DecSm.Atom.Module.Dotnet/Cli/VerbosityOptions.cs b/src/Invex.Atom.Module.Dotnet/Cli/VerbosityOptions.cs similarity index 97% rename from DecSm.Atom.Module.Dotnet/Cli/VerbosityOptions.cs rename to src/Invex.Atom.Module.Dotnet/Cli/VerbosityOptions.cs index 2f517860..fbb1b9e3 100644 --- a/DecSm.Atom.Module.Dotnet/Cli/VerbosityOptions.cs +++ b/src/Invex.Atom.Module.Dotnet/Cli/VerbosityOptions.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.Dotnet.Cli; +namespace Invex.Atom.Module.Dotnet.Cli; /// /// Specifies the verbosity level for .NET CLI commands. diff --git a/DecSm.Atom.Module.Dotnet/Helpers/IDotnetCliHelper.cs b/src/Invex.Atom.Module.Dotnet/Helpers/IDotnetCliHelper.cs similarity index 86% rename from DecSm.Atom.Module.Dotnet/Helpers/IDotnetCliHelper.cs rename to src/Invex.Atom.Module.Dotnet/Helpers/IDotnetCliHelper.cs index 14c5c1ff..31ea8884 100644 --- a/DecSm.Atom.Module.Dotnet/Helpers/IDotnetCliHelper.cs +++ b/src/Invex.Atom.Module.Dotnet/Helpers/IDotnetCliHelper.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.Dotnet.Helpers; +namespace Invex.Atom.Module.Dotnet.Helpers; /// /// Provides access to the .NET CLI for executing various `dotnet` commands. @@ -23,6 +23,6 @@ public partial interface IDotnetCliHelper : IBuildAccessor /// /// This method registers as the singleton implementation for . /// - protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) => + protected static partial void ConfigureBuilderFromIDotnetCliHelper(IHostApplicationBuilder builder) => builder.Services.AddSingleton(); } diff --git a/DecSm.Atom.Module.Dotnet/Helpers/IDotnetPackHelper.cs b/src/Invex.Atom.Module.Dotnet/Helpers/IDotnetPackHelper.cs similarity index 88% rename from DecSm.Atom.Module.Dotnet/Helpers/IDotnetPackHelper.cs rename to src/Invex.Atom.Module.Dotnet/Helpers/IDotnetPackHelper.cs index 6f3bb6b9..8add1289 100644 --- a/DecSm.Atom.Module.Dotnet/Helpers/IDotnetPackHelper.cs +++ b/src/Invex.Atom.Module.Dotnet/Helpers/IDotnetPackHelper.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.Dotnet.Helpers; +namespace Invex.Atom.Module.Dotnet.Helpers; /// /// Provides helper methods for packing .NET projects into NuGet packages and staging them for publishing. @@ -29,7 +29,7 @@ Task DotnetPackAndStage( DotnetPackAndStageOptions? options = null, CancellationToken cancellationToken = default) { - var projectPath = DotnetFileUtil.GetProjectFilePathByName(FileSystem, projectName) ?? + var projectPath = DotnetFileUtil.GetProjectFilePathByName(RootedFileSystem, projectName) ?? throw new StepFailedException($"Could not locate project file for project {projectName}."); Logger.LogDebug("Located project file for project {ProjectName} at {ProjectPath}", projectName, projectPath); @@ -91,17 +91,17 @@ async Task DotnetPackAndStage( var projectName = projectPath.FileNameWithoutExtension; var buildDirectory = options.PackOptions?.Output is { Length: > 0 } - ? FileSystem.CreateRootedPath(options.PackOptions?.Output!) + ? RootedFileSystem.CreateRootedPath(options.PackOptions?.Output!) : projectPath.Parent! / "bin" / configuration; - var publishDirectory = FileSystem.AtomPublishDirectory / projectName; + var publishDirectory = RootedFileSystem.AtomPublishDirectory / projectName; Logger.LogInformation("Packing project {Project}", projectName); - if (FileSystem.Directory.Exists(buildDirectory)) + if (RootedFileSystem.Directory.Exists(buildDirectory)) { Logger.LogDebug("Deleting existing pack directory {PackDirectory}", buildDirectory); - FileSystem.Directory.Delete(buildDirectory, true); + RootedFileSystem.Directory.Delete(buildDirectory, true); } Logger.LogDebug( @@ -115,18 +115,19 @@ options.CustomPropertiesTransform is not null (options.SetVersionsFromProviders, options.CustomPropertiesTransform) switch { (true, not null) => await TransformProjectVersionScope - .CreateAsync(DotnetFileUtil.GetPropertyFilesForProject(projectPath, FileSystem.AtomRootDirectory), + .CreateAsync( + DotnetFileUtil.GetPropertyFilesForProject(projectPath, RootedFileSystem.AtomRootDirectory), BuildVersion, cancellationToken) .AddAsync(options.CustomPropertiesTransform), (true, null) => await TransformProjectVersionScope.CreateAsync( - DotnetFileUtil.GetPropertyFilesForProject(projectPath, FileSystem.AtomRootDirectory), + DotnetFileUtil.GetPropertyFilesForProject(projectPath, RootedFileSystem.AtomRootDirectory), BuildVersion, cancellationToken), (false, not null) => await TransformMultiFileScope.CreateAsync( - DotnetFileUtil.GetPropertyFilesForProject(projectPath, FileSystem.AtomRootDirectory), + DotnetFileUtil.GetPropertyFilesForProject(projectPath, RootedFileSystem.AtomRootDirectory), options.CustomPropertiesTransform!, cancellationToken), @@ -135,7 +136,7 @@ options.CustomPropertiesTransform is not null await DotnetCli.Pack(projectPath, options.PackOptions, cancellationToken: cancellationToken); - var packagedFile = FileSystem.CreateRootedPath(FileSystem + var packagedFile = RootedFileSystem.CreateRootedPath(RootedFileSystem .Directory .GetFiles(buildDirectory, $"{projectName}.*.nupkg") .OrderDescending() @@ -150,21 +151,21 @@ options.CustomPropertiesTransform is not null Logger.LogDebug("Moving package {PackagedFile} to {PublishedFile}", packagedFile, publishedFile); - if (options.ClearPublishDirectory && FileSystem.Directory.Exists(publishDirectory)) + if (options.ClearPublishDirectory && RootedFileSystem.Directory.Exists(publishDirectory)) { Logger.LogDebug("Deleting existing publish directory {PublishDirectory}", publishDirectory); - FileSystem.Directory.Delete(publishDirectory, true); + RootedFileSystem.Directory.Delete(publishDirectory, true); } - FileSystem.Directory.CreateDirectory(publishDirectory); + RootedFileSystem.Directory.CreateDirectory(publishDirectory); - if (FileSystem.File.Exists(publishedFile)) + if (RootedFileSystem.File.Exists(publishedFile)) { Logger.LogDebug("Deleting existing published file {PublishedFile}", publishedFile); - FileSystem.File.Delete(publishedFile); + RootedFileSystem.File.Delete(publishedFile); } - FileSystem.File.Move(packagedFile, publishedFile); + RootedFileSystem.File.Move(packagedFile, publishedFile); Logger.LogInformation("Packed project {Project}", projectName); } diff --git a/DecSm.Atom.Module.Dotnet/Helpers/IDotnetPublishHelper.cs b/src/Invex.Atom.Module.Dotnet/Helpers/IDotnetPublishHelper.cs similarity index 88% rename from DecSm.Atom.Module.Dotnet/Helpers/IDotnetPublishHelper.cs rename to src/Invex.Atom.Module.Dotnet/Helpers/IDotnetPublishHelper.cs index 069fc535..f9765c9e 100644 --- a/DecSm.Atom.Module.Dotnet/Helpers/IDotnetPublishHelper.cs +++ b/src/Invex.Atom.Module.Dotnet/Helpers/IDotnetPublishHelper.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.Dotnet.Helpers; +namespace Invex.Atom.Module.Dotnet.Helpers; /// /// Provides helper methods for publishing .NET projects and staging their output. @@ -29,7 +29,7 @@ Task DotnetPublishAndStage( DotnetPublishAndStageOptions? options = null, CancellationToken cancellationToken = default) { - var projectPath = DotnetFileUtil.GetProjectFilePathByName(FileSystem, projectName) ?? + var projectPath = DotnetFileUtil.GetProjectFilePathByName(RootedFileSystem, projectName) ?? throw new StepFailedException($"Could not locate project file for project {projectName}."); Logger.LogDebug("Located project file for project {ProjectName} at {ProjectPath}", projectName, projectPath); @@ -88,17 +88,17 @@ async Task DotnetPublishAndStage( var projectName = projectPath.FileNameWithoutExtension; var buildDirectory = options.PublishOptions?.Output is { Length: > 0 } - ? FileSystem.CreateRootedPath(options.PublishOptions?.Output!) - : FileSystem.AtomRootDirectory / projectName / configuration / "atom-publish"; + ? RootedFileSystem.CreateRootedPath(options.PublishOptions?.Output!) + : RootedFileSystem.AtomRootDirectory / projectName / configuration / "atom-publish"; - var publishDirectory = FileSystem.AtomPublishDirectory / projectName; + var publishDirectory = RootedFileSystem.AtomPublishDirectory / projectName; Logger.LogInformation("Publishing project {Project}", projectName); - if (FileSystem.Directory.Exists(buildDirectory)) + if (RootedFileSystem.Directory.Exists(buildDirectory)) { Logger.LogDebug("Deleting existing build directory {BuildDirectory}", buildDirectory); - FileSystem.Directory.Delete(buildDirectory, true); + RootedFileSystem.Directory.Delete(buildDirectory, true); } Logger.LogDebug( @@ -112,18 +112,19 @@ options.CustomPropertiesTransform is not null (options.SetVersionsFromProviders, options.CustomPropertiesTransform) switch { (true, not null) => await TransformProjectVersionScope - .CreateAsync(DotnetFileUtil.GetPropertyFilesForProject(projectPath, FileSystem.AtomRootDirectory), + .CreateAsync( + DotnetFileUtil.GetPropertyFilesForProject(projectPath, RootedFileSystem.AtomRootDirectory), BuildVersion, cancellationToken) .AddAsync(options.CustomPropertiesTransform), (true, null) => await TransformProjectVersionScope.CreateAsync( - DotnetFileUtil.GetPropertyFilesForProject(projectPath, FileSystem.AtomRootDirectory), + DotnetFileUtil.GetPropertyFilesForProject(projectPath, RootedFileSystem.AtomRootDirectory), BuildVersion, cancellationToken), (false, not null) => await TransformMultiFileScope.CreateAsync( - DotnetFileUtil.GetPropertyFilesForProject(projectPath, FileSystem.AtomRootDirectory), + DotnetFileUtil.GetPropertyFilesForProject(projectPath, RootedFileSystem.AtomRootDirectory), options.CustomPropertiesTransform!, cancellationToken), @@ -147,16 +148,16 @@ options.PublishOptions is null buildDirectory, publishDirectory); - if (FileSystem.Directory.Exists(publishDirectory)) + if (RootedFileSystem.Directory.Exists(publishDirectory)) { Logger.LogDebug("Deleting existing Atom publish directory {PublishDirectory}", publishDirectory); - FileSystem.Directory.Delete(publishDirectory, true); + RootedFileSystem.Directory.Delete(publishDirectory, true); } - if (!FileSystem.Directory.Exists(publishDirectory.Parent!)) - FileSystem.Directory.CreateDirectory(publishDirectory.Parent!); + if (!RootedFileSystem.Directory.Exists(publishDirectory.Parent!)) + RootedFileSystem.Directory.CreateDirectory(publishDirectory.Parent!); - FileSystem.Directory.Move(buildDirectory, publishDirectory); + RootedFileSystem.Directory.Move(buildDirectory, publishDirectory); Logger.LogInformation("Published project {Project}", projectName); } diff --git a/DecSm.Atom.Module.Dotnet/Helpers/IDotnetTestHelper.cs b/src/Invex.Atom.Module.Dotnet/Helpers/IDotnetTestHelper.cs similarity index 83% rename from DecSm.Atom.Module.Dotnet/Helpers/IDotnetTestHelper.cs rename to src/Invex.Atom.Module.Dotnet/Helpers/IDotnetTestHelper.cs index 9ce83d2f..19028966 100644 --- a/DecSm.Atom.Module.Dotnet/Helpers/IDotnetTestHelper.cs +++ b/src/Invex.Atom.Module.Dotnet/Helpers/IDotnetTestHelper.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.Dotnet.Helpers; +namespace Invex.Atom.Module.Dotnet.Helpers; /// /// Provides helper methods for running .NET tests and staging their results and code coverage reports. @@ -30,7 +30,7 @@ Task DotnetTestAndStage( DotnetTestAndStageOptions? options = null, CancellationToken cancellationToken = default) { - var projectPath = DotnetFileUtil.GetProjectFilePathByName(FileSystem, projectName) ?? + var projectPath = DotnetFileUtil.GetProjectFilePathByName(RootedFileSystem, projectName) ?? throw new StepFailedException($"Could not locate project file for project {projectName}."); Logger.LogDebug("Located project file for project {ProjectName} at {ProjectPath}", projectName, projectPath); @@ -46,6 +46,12 @@ Task DotnetTestAndStage( /// Optional. Configuration options for the testing and staging process. /// A cancellation token to observe while waiting for the task to complete. /// A that returns the exit code of the `dotnet test` command. + /// + /// Thrown when the `dotnet test` invocation fails before producing any test results (e.g. a build error, + /// invalid arguments, or a crashed test host), as opposed to the run completing with failing tests. This is + /// detected by the absence of a TRX results file on a non-zero exit code. Failing tests do not throw and + /// instead surface via the returned exit code and the generated test report. + /// /// /// /// This method performs the following steps: @@ -107,7 +113,10 @@ async Task DotnetTestAndStage( { Output = testOptions.Output is { Length: > 0 } ? testOptions.Output - : FileSystem.AtomRootDirectory / projectName / "TestResults", + : RootedFileSystem.AtomRootDirectory / projectName / "TestResults", + ResultsDirectory = testOptions.ResultsDirectory is { Length: > 0 } + ? testOptions.ResultsDirectory + : RootedFileSystem.AtomRootDirectory / projectName / "TestResults", Logger = testOptions.Logger ?? [ $"\"trx;LogFileName={projectName}.trx\"", $"\"html;LogFileName={projectName}.html\"", @@ -118,31 +127,31 @@ async Task DotnetTestAndStage( : null), }; - var testOutputDirectory = FileSystem.CreateRootedPath(testOptions.Output); + var testOutputDirectory = RootedFileSystem.CreateRootedPath(testOptions.Output); - var publishDirectory = FileSystem.AtomPublishDirectory / projectName; + var publishDirectory = RootedFileSystem.AtomPublishDirectory / projectName; var testResultsPublishDirectory = publishDirectory / "test-results"; - if (FileSystem.Directory.Exists(testResultsPublishDirectory)) - FileSystem.Directory.Delete(testResultsPublishDirectory, true); + if (RootedFileSystem.Directory.Exists(testResultsPublishDirectory)) + RootedFileSystem.Directory.Delete(testResultsPublishDirectory, true); - FileSystem.Directory.CreateDirectory(testResultsPublishDirectory); + RootedFileSystem.Directory.CreateDirectory(testResultsPublishDirectory); var coverageResultsPublishDirectory = publishDirectory / "coverage-results"; - if (options.IncludeCoverage && FileSystem.Directory.Exists(coverageResultsPublishDirectory)) - FileSystem.Directory.Delete(coverageResultsPublishDirectory, true); + if (options.IncludeCoverage && RootedFileSystem.Directory.Exists(coverageResultsPublishDirectory)) + RootedFileSystem.Directory.Delete(coverageResultsPublishDirectory, true); if (options.IncludeCoverage) - FileSystem.Directory.CreateDirectory(coverageResultsPublishDirectory); + RootedFileSystem.Directory.CreateDirectory(coverageResultsPublishDirectory); Logger.LogInformation("Running unit tests for project {Project}", projectName); - if (FileSystem.Directory.Exists(testOutputDirectory)) + if (RootedFileSystem.Directory.Exists(testOutputDirectory)) { Logger.LogDebug("Deleting existing test output directory {TestOutputDirectory}", testOutputDirectory); - FileSystem.Directory.Delete(testOutputDirectory, true); + RootedFileSystem.Directory.Delete(testOutputDirectory, true); } Logger.LogDebug( @@ -156,18 +165,19 @@ options.CustomPropertiesTransform is not null (options.SetVersionsFromProviders, options.CustomPropertiesTransform) switch { (true, not null) => await TransformProjectVersionScope - .CreateAsync(DotnetFileUtil.GetPropertyFilesForProject(projectPath, FileSystem.AtomRootDirectory), + .CreateAsync( + DotnetFileUtil.GetPropertyFilesForProject(projectPath, RootedFileSystem.AtomRootDirectory), BuildVersion, cancellationToken) .AddAsync(options.CustomPropertiesTransform), (true, null) => await TransformProjectVersionScope.CreateAsync( - DotnetFileUtil.GetPropertyFilesForProject(projectPath, FileSystem.AtomRootDirectory), + DotnetFileUtil.GetPropertyFilesForProject(projectPath, RootedFileSystem.AtomRootDirectory), BuildVersion, cancellationToken), (false, not null) => await TransformMultiFileScope.CreateAsync( - DotnetFileUtil.GetPropertyFilesForProject(projectPath, FileSystem.AtomRootDirectory), + DotnetFileUtil.GetPropertyFilesForProject(projectPath, RootedFileSystem.AtomRootDirectory), options.CustomPropertiesTransform!, cancellationToken), @@ -181,17 +191,27 @@ options.CustomPropertiesTransform is not null TransformError = s => s.Contains(", is an invalid character") ? null : s, + AllowFailedResult = true, }, cancellationToken); + // A non-zero exit code can mean either "tests failed" or "the dotnet test invocation itself failed" + // (e.g. build error, missing project, crashed test host). These share exit codes and cannot be reliably + // distinguished by code alone. The TRX file is only produced when tests actually ran, so its absence on a + // failed result indicates the invocation failed before running any tests - in which case we throw rather + // than swallowing the failure and returning a misleading exit code. + var trxFile = testOutputDirectory / $"{projectName}.trx"; + + if (result.ExitCode is not 0 && !RootedFileSystem.File.Exists(trxFile)) + throw new StepFailedException( + $"dotnet test failed for project {projectName} with exit code {result.ExitCode} before producing any test results. " + + "This indicates the test run could not start (e.g. a build error or invalid arguments) rather than failing tests."); + // Copy html file to publish directory - FileSystem.File.Copy(testOutputDirectory / $"{projectName}.html", + RootedFileSystem.File.Copy(testOutputDirectory / $"{projectName}.html", testResultsPublishDirectory / $"{projectName}.html"); - GenerateTestReport(projectName, - testOptions.Configuration, - testOptions.Framework, - testOutputDirectory / $"{projectName}.trx"); + GenerateTestReport(projectName, testOptions.Configuration, testOptions.Framework, trxFile); if (!options.IncludeCoverage) return result.ExitCode; @@ -201,7 +221,7 @@ await DotnetCli.ToolExecute("dotnet-reportgenerator-globaltool", $"-reports:{testOutputDirectory / "**" / "coverage.cobertura.xml"}", $"-targetdir:{coverageResultsPublishDirectory}", "-reporttypes:HtmlInline;JsonSummary", - "-sourcedirs:" + FileSystem.AtomRootDirectory, + "-sourcedirs:" + RootedFileSystem.AtomRootDirectory, ], new() { @@ -209,7 +229,7 @@ await DotnetCli.ToolExecute("dotnet-reportgenerator-globaltool", }, new("", "") { - AllowFailedResult = true, + AllowFailedResult = false, }, cancellationToken); @@ -303,7 +323,7 @@ void GenerateCoverageReport( string coverageJsonFile, bool includeTitle = true) { - var coverageJson = FileSystem.File.ReadAllText(coverageJsonFile); + var coverageJson = RootedFileSystem.File.ReadAllText(coverageJsonFile); var summary = JsonSerializer.Deserialize(coverageJson, CoverageModelContext.Default.CoverageModel)!.Summary; diff --git a/DecSm.Atom.Module.Dotnet/Helpers/IDotnetToolInstallHelper.cs b/src/Invex.Atom.Module.Dotnet/Helpers/IDotnetToolInstallHelper.cs similarity index 94% rename from DecSm.Atom.Module.Dotnet/Helpers/IDotnetToolInstallHelper.cs rename to src/Invex.Atom.Module.Dotnet/Helpers/IDotnetToolInstallHelper.cs index b99b8354..ddcd02ce 100644 --- a/DecSm.Atom.Module.Dotnet/Helpers/IDotnetToolInstallHelper.cs +++ b/src/Invex.Atom.Module.Dotnet/Helpers/IDotnetToolInstallHelper.cs @@ -1,11 +1,11 @@ -namespace DecSm.Atom.Module.Dotnet.Helpers; +namespace Invex.Atom.Module.Dotnet.Helpers; /// /// Provides helper methods for installing and managing .NET CLI tools. /// /// /// This interface extends to gain access to core build services -/// like and . +/// like and . /// [PublicAPI] public interface IDotnetToolInstallHelper : IBuildAccessor @@ -42,7 +42,8 @@ void InstallTool(string toolName, string? version = null, bool global = true, bo ? "-g" : string.Empty; - if (!global && !FileSystem.File.Exists(FileSystem.CurrentDirectory / ".config" / "dotnet-tools.json")) + if (!global && + !RootedFileSystem.File.Exists(RootedFileSystem.CurrentDirectory / ".config" / "dotnet-tools.json")) ProcessRunner.Run(new("dotnet", "new tool-manifest") { InvocationLogLevel = LogLevel.Debug, @@ -122,7 +123,8 @@ async Task InstallToolAsync( ? "-g" : string.Empty; - if (!global && !FileSystem.File.Exists(FileSystem.CurrentDirectory / ".config" / "dotnet-tools.json")) + if (!global && + !RootedFileSystem.File.Exists(RootedFileSystem.CurrentDirectory / ".config" / "dotnet-tools.json")) await ProcessRunner.RunAsync(new("dotnet", "new tool-manifest") { InvocationLogLevel = LogLevel.Debug, diff --git a/DecSm.Atom.Module.Dotnet/Helpers/INugetHelper.cs b/src/Invex.Atom.Module.Dotnet/Helpers/INugetHelper.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Helpers/INugetHelper.cs rename to src/Invex.Atom.Module.Dotnet/Helpers/INugetHelper.cs index edeb88bc..5f8e753e 100644 --- a/DecSm.Atom.Module.Dotnet/Helpers/INugetHelper.cs +++ b/src/Invex.Atom.Module.Dotnet/Helpers/INugetHelper.cs @@ -1,11 +1,11 @@ -namespace DecSm.Atom.Module.Dotnet.Helpers; +namespace Invex.Atom.Module.Dotnet.Helpers; /// /// Provides helper methods for interacting with NuGet, such as pushing packages and managing NuGet configuration. /// /// /// This interface extends to leverage build version information -/// and provides functionality for common NuGet operations within a DecSm.Atom build. +/// and provides functionality for common NuGet operations within a Invex.Atom build. /// [PublicAPI] public interface INugetHelper : IBuildInfo @@ -36,7 +36,7 @@ RootedPath NugetConfigPath // Linux: $HOME/.nuget/NuGet.Config // Mac: $HOME/.nuget/NuGet.Config var appDataPath = - FileSystem.CreateRootedPath(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); + RootedFileSystem.CreateRootedPath(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)); return Environment.OSVersion.Platform switch { @@ -76,8 +76,8 @@ async Task PushProject( RootedPath? configFile = null, CancellationToken cancellationToken = default) { - var packageBuildDir = FileSystem.AtomArtifactsDirectory / projectName; - var packages = FileSystem.Directory.GetFiles(packageBuildDir, "*.nupkg"); + var packageBuildDir = RootedFileSystem.AtomArtifactsDirectory / projectName; + var packages = RootedFileSystem.Directory.GetFiles(packageBuildDir, "*.nupkg"); if (packages.Length == 0) { @@ -252,7 +252,7 @@ async Task CreateNugetConfigOverwriteScope( { if (skipIfExists) { - var nugetContents = await FileSystem.File.ReadAllTextAsync(NugetConfigPath, cancellationToken); + var nugetContents = await RootedFileSystem.File.ReadAllTextAsync(NugetConfigPath, cancellationToken); if (feeds.All(f => nugetContents.Contains(f.Url, StringComparison.OrdinalIgnoreCase))) { diff --git a/DecSm.Atom.Module.Dotnet/DecSm.Atom.Module.Dotnet.csproj b/src/Invex.Atom.Module.Dotnet/Invex.Atom.Module.Dotnet.csproj similarity index 71% rename from DecSm.Atom.Module.Dotnet/DecSm.Atom.Module.Dotnet.csproj rename to src/Invex.Atom.Module.Dotnet/Invex.Atom.Module.Dotnet.csproj index f546829a..92de5341 100644 --- a/DecSm.Atom.Module.Dotnet/DecSm.Atom.Module.Dotnet.csproj +++ b/src/Invex.Atom.Module.Dotnet/Invex.Atom.Module.Dotnet.csproj @@ -6,6 +6,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -21,12 +22,12 @@ - - + + - + \ No newline at end of file diff --git a/src/Invex.Atom.Module.Dotnet/Invex.Atom.Module.Dotnet.props b/src/Invex.Atom.Module.Dotnet/Invex.Atom.Module.Dotnet.props new file mode 100644 index 00000000..2e6944f1 --- /dev/null +++ b/src/Invex.Atom.Module.Dotnet/Invex.Atom.Module.Dotnet.props @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/DecSm.Atom.Module.Dotnet/Model/CoverageModels.cs b/src/Invex.Atom.Module.Dotnet/Model/CoverageModels.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Model/CoverageModels.cs rename to src/Invex.Atom.Module.Dotnet/Model/CoverageModels.cs index 0b51a9fa..5f9da449 100644 --- a/DecSm.Atom.Module.Dotnet/Model/CoverageModels.cs +++ b/src/Invex.Atom.Module.Dotnet/Model/CoverageModels.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.Dotnet.Model; +namespace Invex.Atom.Module.Dotnet.Model; /// /// Represents the root model for code coverage summary data, typically deserialized from a JSON report. diff --git a/DecSm.Atom.Module.Dotnet/Model/NugetFeed.cs b/src/Invex.Atom.Module.Dotnet/Model/NugetFeed.cs similarity index 98% rename from DecSm.Atom.Module.Dotnet/Model/NugetFeed.cs rename to src/Invex.Atom.Module.Dotnet/Model/NugetFeed.cs index 157854e8..bc81749e 100644 --- a/DecSm.Atom.Module.Dotnet/Model/NugetFeed.cs +++ b/src/Invex.Atom.Module.Dotnet/Model/NugetFeed.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.Dotnet.Model; +namespace Invex.Atom.Module.Dotnet.Model; /// /// Represents a NuGet feed configuration, including its URL, name, and optional credentials. diff --git a/DecSm.Atom.Module.Dotnet/Model/TrxModels.cs b/src/Invex.Atom.Module.Dotnet/Model/TrxModels.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Model/TrxModels.cs rename to src/Invex.Atom.Module.Dotnet/Model/TrxModels.cs index 9cad8aa6..ff3aa78b 100644 --- a/DecSm.Atom.Module.Dotnet/Model/TrxModels.cs +++ b/src/Invex.Atom.Module.Dotnet/Model/TrxModels.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.Dotnet.Model; +namespace Invex.Atom.Module.Dotnet.Model; /// /// Represents the root element of a TRX (Test Results XML) file. diff --git a/DecSm.Atom.Module.Dotnet/Util/DotnetFileUtil.cs b/src/Invex.Atom.Module.Dotnet/Util/DotnetFileUtil.cs similarity index 93% rename from DecSm.Atom.Module.Dotnet/Util/DotnetFileUtil.cs rename to src/Invex.Atom.Module.Dotnet/Util/DotnetFileUtil.cs index 97129118..fa4d5317 100644 --- a/DecSm.Atom.Module.Dotnet/Util/DotnetFileUtil.cs +++ b/src/Invex.Atom.Module.Dotnet/Util/DotnetFileUtil.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.Dotnet.Util; +namespace Invex.Atom.Module.Dotnet.Util; [PublicAPI] public static class DotnetFileUtil @@ -42,7 +42,7 @@ public static IEnumerable GetPropertyFilesForProject(RootedPath proj /// Searches for a file matching the pattern {projectName}.*proj in all subdirectories of the root directory. /// If multiple files match, the one with the shortest path ancestry (fewest directory levels) is returned. /// - public static RootedPath? GetProjectFilePathByName(IAtomFileSystem fileSystem, string projectName) + public static RootedPath? GetProjectFilePathByName(IRootedFileSystem fileSystem, string projectName) { var foundProjectPath = fileSystem .Directory diff --git a/DecSm.Atom.Module.Dotnet/Util/MsBuildUtil.cs b/src/Invex.Atom.Module.Dotnet/Util/MsBuildUtil.cs similarity index 99% rename from DecSm.Atom.Module.Dotnet/Util/MsBuildUtil.cs rename to src/Invex.Atom.Module.Dotnet/Util/MsBuildUtil.cs index 0baf5174..8cd4df61 100644 --- a/DecSm.Atom.Module.Dotnet/Util/MsBuildUtil.cs +++ b/src/Invex.Atom.Module.Dotnet/Util/MsBuildUtil.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.Dotnet.Util; +namespace Invex.Atom.Module.Dotnet.Util; /// /// Provides utility methods for working with MSBuild project files, specifically for managing version information. diff --git a/DecSm.Atom.Module.Dotnet/Util/TransformProjectVersionScope.cs b/src/Invex.Atom.Module.Dotnet/Util/TransformProjectVersionScope.cs similarity index 96% rename from DecSm.Atom.Module.Dotnet/Util/TransformProjectVersionScope.cs rename to src/Invex.Atom.Module.Dotnet/Util/TransformProjectVersionScope.cs index dd4ce260..94b1109b 100644 --- a/DecSm.Atom.Module.Dotnet/Util/TransformProjectVersionScope.cs +++ b/src/Invex.Atom.Module.Dotnet/Util/TransformProjectVersionScope.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.Dotnet.Util; +namespace Invex.Atom.Module.Dotnet.Util; [PublicAPI] public static class TransformProjectVersionScope diff --git a/DecSm.Atom.Module.Dotnet/_usings.cs b/src/Invex.Atom.Module.Dotnet/_usings.cs similarity index 53% rename from DecSm.Atom.Module.Dotnet/_usings.cs rename to src/Invex.Atom.Module.Dotnet/_usings.cs index 0d70e596..a16533e7 100644 --- a/DecSm.Atom.Module.Dotnet/_usings.cs +++ b/src/Invex.Atom.Module.Dotnet/_usings.cs @@ -7,17 +7,17 @@ global using System.Text.Json.Serialization; global using System.Xml; global using System.Xml.Serialization; -global using DecSm.Atom.Args; -global using DecSm.Atom.Build; -global using DecSm.Atom.BuildInfo; -global using DecSm.Atom.Hosting; -global using DecSm.Atom.Module.Dotnet.Cli; -global using DecSm.Atom.Module.Dotnet.Model; -global using DecSm.Atom.Module.Dotnet.Util; -global using DecSm.Atom.Params; -global using DecSm.Atom.Paths; -global using DecSm.Atom.Process; -global using DecSm.Atom.Reports; +global using Invex.Atom.Build.Args; +global using Invex.Atom.Build; +global using Invex.Atom.Build.BuildInfo; +global using Invex.Atom.Build.Params; +global using Invex.Atom.Build.Exceptions; +global using Invex.Atom.Build.FileSystem; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Module.Dotnet.Cli; +global using Invex.Atom.Module.Dotnet.Model; +global using Invex.Atom.Module.Dotnet.Util; +global using Invex.Atom.Build.Reports; global using JetBrains.Annotations; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Hosting; diff --git a/src/Invex.Atom.Module.GitVersion/Flags/BuildFlagsExtensions.cs b/src/Invex.Atom.Module.GitVersion/Flags/BuildFlagsExtensions.cs new file mode 100644 index 00000000..ec7d7211 --- /dev/null +++ b/src/Invex.Atom.Module.GitVersion/Flags/BuildFlagsExtensions.cs @@ -0,0 +1,33 @@ +namespace Invex.Atom.Module.GitVersion.Flags; + +[PublicAPI] +[SuppressMessage("Performance", "CA1822:Mark members as static")] +public static class BuildFlagsExtensions +{ + [PublicAPI] + public sealed class GitVersionFlags + { + public static GitVersionFlags Instance => field ??= new(); + + public GitVersionProvideBuildIdFlag ProvideBuildId => field ??= new(); + + public GitVersionProvideBuildVersionFlag ProvideBuildVersion => field ??= new(); + + public GitVersionProvideBuildIdFlag SetProvideBuildId(bool value) => + new() + { + Enabled = value, + }; + + public GitVersionProvideBuildVersionFlag SetProvideBuildVersion(bool value) => + new() + { + Enabled = value, + }; + } + + extension(BuildOptions) + { + public static GitVersionFlags GitVersion => GitVersionFlags.Instance; + } +} diff --git a/src/Invex.Atom.Module.GitVersion/Flags/GitVersionProvideBuildIdFlag.cs b/src/Invex.Atom.Module.GitVersion/Flags/GitVersionProvideBuildIdFlag.cs new file mode 100644 index 00000000..b7fc50eb --- /dev/null +++ b/src/Invex.Atom.Module.GitVersion/Flags/GitVersionProvideBuildIdFlag.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Module.GitVersion.Flags; + +[PublicAPI] +public sealed record GitVersionProvideBuildIdFlag : ToggleBuildOption; diff --git a/src/Invex.Atom.Module.GitVersion/Flags/GitVersionProvideBuildVersionFlag.cs b/src/Invex.Atom.Module.GitVersion/Flags/GitVersionProvideBuildVersionFlag.cs new file mode 100644 index 00000000..c5970f8b --- /dev/null +++ b/src/Invex.Atom.Module.GitVersion/Flags/GitVersionProvideBuildVersionFlag.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Module.GitVersion.Flags; + +[PublicAPI] +public sealed record GitVersionProvideBuildVersionFlag : ToggleBuildOption; diff --git a/src/Invex.Atom.Module.GitVersion/IGitVersion.cs b/src/Invex.Atom.Module.GitVersion/IGitVersion.cs new file mode 100644 index 00000000..61c5a2f4 --- /dev/null +++ b/src/Invex.Atom.Module.GitVersion/IGitVersion.cs @@ -0,0 +1,12 @@ +namespace Invex.Atom.Module.GitVersion; + +[PublicAPI] +[ConfigureHostBuilder] +public partial interface IGitVersion +{ + protected static partial void ConfigureBuilderFromIGitVersion(IHostApplicationBuilder builder) => + builder + .Services + .AddSingleton() + .AddSingleton(); +} diff --git a/DecSm.Atom.Module.GitVersion/DecSm.Atom.Module.GitVersion.csproj b/src/Invex.Atom.Module.GitVersion/Invex.Atom.Module.GitVersion.csproj similarity index 72% rename from DecSm.Atom.Module.GitVersion/DecSm.Atom.Module.GitVersion.csproj rename to src/Invex.Atom.Module.GitVersion/Invex.Atom.Module.GitVersion.csproj index 8ea52601..2a740290 100644 --- a/DecSm.Atom.Module.GitVersion/DecSm.Atom.Module.GitVersion.csproj +++ b/src/Invex.Atom.Module.GitVersion/Invex.Atom.Module.GitVersion.csproj @@ -5,6 +5,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -20,12 +21,12 @@ - - + + - + \ No newline at end of file diff --git a/src/Invex.Atom.Module.GitVersion/Invex.Atom.Module.GitVersion.props b/src/Invex.Atom.Module.GitVersion/Invex.Atom.Module.GitVersion.props new file mode 100644 index 00000000..4fce70a0 --- /dev/null +++ b/src/Invex.Atom.Module.GitVersion/Invex.Atom.Module.GitVersion.props @@ -0,0 +1,7 @@ + + + + + + + diff --git a/DecSm.Atom.Module.GitVersion/JsonElementContext.cs b/src/Invex.Atom.Module.GitVersion/JsonElementContext.cs similarity index 92% rename from DecSm.Atom.Module.GitVersion/JsonElementContext.cs rename to src/Invex.Atom.Module.GitVersion/JsonElementContext.cs index 1938e6d9..f3e62bc3 100644 --- a/DecSm.Atom.Module.GitVersion/JsonElementContext.cs +++ b/src/Invex.Atom.Module.GitVersion/JsonElementContext.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.GitVersion; +namespace Invex.Atom.Module.GitVersion; /// /// Provides a for source generation of . diff --git a/DecSm.Atom.Module.GitVersion/GitVersionBuildIdProvider.cs b/src/Invex.Atom.Module.GitVersion/Providers/GitVersionBuildIdProvider.cs similarity index 69% rename from DecSm.Atom.Module.GitVersion/GitVersionBuildIdProvider.cs rename to src/Invex.Atom.Module.GitVersion/Providers/GitVersionBuildIdProvider.cs index 59fde50f..95b9e91b 100644 --- a/DecSm.Atom.Module.GitVersion/GitVersionBuildIdProvider.cs +++ b/src/Invex.Atom.Module.GitVersion/Providers/GitVersionBuildIdProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.GitVersion; +namespace Invex.Atom.Module.GitVersion.Providers; /// /// Provides the build ID using GitVersion. @@ -11,7 +11,7 @@ internal sealed class GitVersionBuildIdProvider( IDotnetToolInstallHelper dotnetToolInstallHelper, IProcessRunner processRunner, IBuildDefinition buildDefinition, - IAtomFileSystem fileSystem, + IRootedFileSystem fileSystem, ILogger logger ) : IBuildIdProvider { @@ -25,7 +25,7 @@ ILogger logger /// Gets the build ID, derived from GitVersion's FullSemVer. /// /// - /// Thrown if GitVersion is not enabled via + /// Thrown if GitVersion is not enabled via /// or if the build ID cannot be determined from GitVersion's output. /// [field: AllowNull] @@ -34,9 +34,9 @@ public string BuildId { get { - if (!UseGitVersionForBuildId.IsEnabled(IWorkflowOption.GetOptionsForCurrentTarget(buildDefinition))) + if (!GitVersionProvideBuildIdFlag.IsEnabled(buildDefinition)) throw new InvalidOperationException( - "GitVersion is not enabled for build ID generation. Ensure UseGitVersionForBuildId is enabled."); + "GitVersion is not enabled for build ID generation. Ensure GitVersionProvideBuildIdFlag is enabled."); if (field is { Length: > 0 }) return field; @@ -49,26 +49,9 @@ public string BuildId .Output .Trim(); - var hashCache = fileSystem.AtomPublishDirectory / ".gitversioncache" / currentGitHash; - - JsonElement? jsonOutput = null; - lock (_lock) { - if (fileSystem.File.Exists(hashCache)) - try - { - var cachedContent = fileSystem.File.ReadAllText(hashCache); - jsonOutput = JsonSerializer.Deserialize(cachedContent, JsonElementContext.Default.JsonElement); - } - catch (Exception ex) - { - logger.LogWarning(ex, - "Failed to read or parse cached GitVersion output. Will re-run GitVersion."); - - jsonOutput = null; - fileSystem.File.Delete(hashCache); - } + var jsonOutput = GitVersionCache.TryRead(fileSystem, currentGitHash, logger); if (jsonOutput is null) { @@ -82,8 +65,7 @@ public string BuildId jsonOutput = JsonSerializer.Deserialize(gitVersionResult.Output, JsonElementContext.Default.JsonElement); - fileSystem.Directory.CreateDirectory(fileSystem.AtomPublishDirectory / ".gitversioncache"); - fileSystem.File.WriteAllText(hashCache, jsonOutput.Value.GetRawText()); + GitVersionCache.Write(fileSystem, currentGitHash, jsonOutput.Value); } var buildId = jsonOutput diff --git a/DecSm.Atom.Module.GitVersion/GitVersionBuildVersionProvider.cs b/src/Invex.Atom.Module.GitVersion/Providers/GitVersionBuildVersionProvider.cs similarity index 71% rename from DecSm.Atom.Module.GitVersion/GitVersionBuildVersionProvider.cs rename to src/Invex.Atom.Module.GitVersion/Providers/GitVersionBuildVersionProvider.cs index f2f40c02..aa9dd4a5 100644 --- a/DecSm.Atom.Module.GitVersion/GitVersionBuildVersionProvider.cs +++ b/src/Invex.Atom.Module.GitVersion/Providers/GitVersionBuildVersionProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.GitVersion; +namespace Invex.Atom.Module.GitVersion.Providers; /// /// Provides the build version using GitVersion. @@ -12,7 +12,8 @@ internal sealed class GitVersionBuildVersionProvider( IDotnetToolInstallHelper dotnetToolInstallHelper, IProcessRunner processRunner, - IAtomFileSystem fileSystem, + IBuildDefinition buildDefinition, + IRootedFileSystem fileSystem, ILogger logger ) : IBuildVersionProvider { @@ -32,6 +33,10 @@ public SemVer Version { get { + if (!GitVersionProvideBuildVersionFlag.IsEnabled(buildDefinition)) + throw new InvalidOperationException( + "GitVersion is not enabled for build version generation. Ensure GitVersionProvideBuildVersionFlag is enabled."); + if (field is not null) return field; @@ -43,26 +48,9 @@ public SemVer Version .Output .Trim(); - var hashCache = fileSystem.AtomPublishDirectory / ".gitversioncache" / currentGitHash; - - JsonElement? jsonOutput = null; - lock (_lock) { - if (fileSystem.File.Exists(hashCache)) - try - { - var cachedContent = fileSystem.File.ReadAllText(hashCache); - jsonOutput = JsonSerializer.Deserialize(cachedContent, JsonElementContext.Default.JsonElement); - } - catch (Exception ex) - { - logger.LogWarning(ex, - "Failed to read or parse cached GitVersion output. Will re-run GitVersion."); - - jsonOutput = null; - fileSystem.File.Delete(hashCache); - } + var jsonOutput = GitVersionCache.TryRead(fileSystem, currentGitHash, logger); if (jsonOutput is null) { @@ -76,8 +64,7 @@ public SemVer Version jsonOutput = JsonSerializer.Deserialize(gitVersionResult.Output, JsonElementContext.Default.JsonElement); - fileSystem.Directory.CreateDirectory(fileSystem.AtomPublishDirectory / ".gitversioncache"); - fileSystem.File.WriteAllText(hashCache, jsonOutput.Value.GetRawText()); + GitVersionCache.Write(fileSystem, currentGitHash, jsonOutput.Value); } var majorProp = jsonOutput diff --git a/src/Invex.Atom.Module.GitVersion/Providers/GitVersionCache.cs b/src/Invex.Atom.Module.GitVersion/Providers/GitVersionCache.cs new file mode 100644 index 00000000..e3c6a1cd --- /dev/null +++ b/src/Invex.Atom.Module.GitVersion/Providers/GitVersionCache.cs @@ -0,0 +1,93 @@ +namespace Invex.Atom.Module.GitVersion.Providers; + +/// +/// Reads and writes a single-file cache of GitVersion output, keyed by the current Git commit hash. +/// +/// +/// The cache is a single file in the build project's obj directory (so it is git-ignored and removed +/// by dotnet clean). The first line stores the Git hash the output was generated for, and the +/// remainder stores the raw GitVersion JSON. When the current hash no longer matches, the cache is ignored +/// and overwritten - just like the tool's restore/build caches, which each track a single hash file rather +/// than one file per key. +/// +internal static class GitVersionCache +{ + private const string CacheFileName = ".gitversioncache"; + + /// + /// Returns the cached GitVersion output if it was generated for ; otherwise + /// null. + /// + internal static JsonElement? TryRead(IRootedFileSystem fileSystem, string gitHash, ILogger logger) + { + var cacheFile = GetCacheFile(fileSystem); + + if (!fileSystem.File.Exists(cacheFile)) + return null; + + try + { + var content = fileSystem.File.ReadAllText(cacheFile); + var newlineIndex = content.IndexOf('\n'); + + if (newlineIndex < 0) + return null; + + var cachedHash = content[..newlineIndex] + .Trim(); + + // The cached output belongs to a different commit - treat it as a miss. + if (!string.Equals(cachedHash, gitHash, StringComparison.OrdinalIgnoreCase)) + return null; + + return JsonSerializer.Deserialize(content[(newlineIndex + 1)..], JsonElementContext.Default.JsonElement); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to read or parse cached GitVersion output. Will re-run GitVersion."); + + try + { + fileSystem.File.Delete(cacheFile); + } + catch (IOException) + { + // Best-effort cleanup; a locked file should never break the build. + } + + return null; + } + } + + /// + /// Persists the GitVersion output for , overwriting any previous entry. + /// + internal static void Write(IRootedFileSystem fileSystem, string gitHash, JsonElement output) + { + fileSystem.Directory.CreateDirectory(ResolveObjDirectory(fileSystem)); + fileSystem.File.WriteAllText(GetCacheFile(fileSystem), $"{gitHash}\n{output.GetRawText()}"); + } + + private static RootedPath GetCacheFile(IRootedFileSystem fileSystem) => + ResolveObjDirectory(fileSystem) / CacheFileName; + + private static RootedPath ResolveObjDirectory(IRootedFileSystem fileSystem) + { + // The build runs from '/bin///', so walk up from the running assembly's + // directory to find the project directory (the first ancestor that contains an 'obj' folder). + var current = fileSystem.CreateRootedPath(AppContext.BaseDirectory); + + while (current.Parent is { } parent) + { + var objDir = parent / "obj"; + + if (objDir.DirectoryExists) + return objDir; + + current = parent; + } + + // Fallback: use an 'obj' directory under the atom root if the project's obj cannot be located. + return fileSystem.AtomRootDirectory / "obj"; + } +} diff --git a/src/Invex.Atom.Module.GitVersion/_usings.cs b/src/Invex.Atom.Module.GitVersion/_usings.cs new file mode 100644 index 00000000..538c5511 --- /dev/null +++ b/src/Invex.Atom.Module.GitVersion/_usings.cs @@ -0,0 +1,15 @@ +global using System.Diagnostics.CodeAnalysis; +global using System.Text.Json; +global using System.Text.Json.Serialization; +global using Invex.Atom.Build.BuildInfo; +global using Invex.Atom.Build.BuildOptions; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.FileSystem; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Module.Dotnet.Helpers; +global using Invex.Atom.Module.GitVersion.Flags; +global using Invex.Atom.Module.GitVersion.Providers; +global using JetBrains.Annotations; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; diff --git a/src/Invex.Atom.Module.GithubWorkflows/DependabotConfig/DependabotConfigFileWriter.cs b/src/Invex.Atom.Module.GithubWorkflows/DependabotConfig/DependabotConfigFileWriter.cs new file mode 100644 index 00000000..41ca162c --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/DependabotConfig/DependabotConfigFileWriter.cs @@ -0,0 +1,34 @@ +namespace Invex.Atom.Module.GithubWorkflows.DependabotConfig; + +/// +/// Writes Dependabot configuration files in YAML format. +/// +[PublicAPI] +public sealed class DependabotConfigFileWriter(IRootedFileSystem fileSystem, ILogger logger) + : WorkflowFileWriter(fileSystem, logger) +{ + private readonly IRootedFileSystem _fileSystem = fileSystem; + private readonly DependabotConfigWriter _configWriter = new(); + + protected override string FileExtension => "yml"; + + protected override int TabSize => 2; + + protected override RootedPath FileLocation => _fileSystem.AtomRootDirectory / ".github"; + + protected override string WriteWorkflow(WorkflowModel workflow) + { + var config = workflow + .Options + .OfType() + .FirstOrDefault(); + + if (config is null) + throw new InvalidOperationException( + $"Dependabot workflow '{workflow.Name}' is missing a {nameof(DependabotConfigOption)}."); + + _configWriter.WriteConfig(config.Config); + + return _configWriter.TextWriter.ToString(); + } +} diff --git a/src/Invex.Atom.Module.GithubWorkflows/DependabotConfig/DependabotConfigOption.cs b/src/Invex.Atom.Module.GithubWorkflows/DependabotConfig/DependabotConfigOption.cs new file mode 100644 index 00000000..55e84204 --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/DependabotConfig/DependabotConfigOption.cs @@ -0,0 +1,5 @@ +namespace Invex.Atom.Module.GithubWorkflows.DependabotConfig; + +[PublicAPI] +public record DependabotConfigOption(StructuredText.GithubActions.DependabotConfigModel.Model.DependabotConfig Config) + : IBuildOption; diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/DependabotWorkflowType.cs b/src/Invex.Atom.Module.GithubWorkflows/DependabotConfig/DependabotWorkflowType.cs similarity index 71% rename from DecSm.Atom.Module.GithubWorkflows/Generation/DependabotWorkflowType.cs rename to src/Invex.Atom.Module.GithubWorkflows/DependabotConfig/DependabotWorkflowType.cs index a135e45d..25965ab8 100644 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/DependabotWorkflowType.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/DependabotConfig/DependabotWorkflowType.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Generation; +namespace Invex.Atom.Module.GithubWorkflows.DependabotConfig; [PublicAPI] public sealed record DependabotWorkflowType : IWorkflowType diff --git a/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowExpressionExtensions.cs b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowExpressionExtensions.cs new file mode 100644 index 00000000..c86123e6 --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowExpressionExtensions.cs @@ -0,0 +1,620 @@ +namespace Invex.Atom.Module.GithubWorkflows.Extensions; + +[PublicAPI] +[SuppressMessage("Performance", "CA1822:Mark members as static")] +public static class WorkflowExpressionExtensions +{ + [PublicAPI] + public sealed class Expressions + { + internal static Expressions Instance => field ??= new(); + + // Github + + /// + /// The github context contains information about the workflow run and the event that triggered the run. You can read + /// most of the github context data in environment variables. For more information about environment variables, see + /// Store information in variables. + /// + public RawExpression Github => field ??= new("github"); + + /// + /// The name of the action currently running, or the + /// + /// id + /// + /// of a step. GitHub removes + /// special characters, and uses the name __run when the current step runs a script without an id. If you + /// use the + /// same action more than once in the same job, the name will include a suffix with the sequence number with underscore + /// before it. For example, the first script you run will have the name __run, and the second script will be + /// named + /// __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2. + /// + public RawExpression GithubAction => field ??= new("github.action"); + + /// + /// The path where an action is located. This property is only supported in composite actions. You can use this path to + /// access files located in the same repository as the action, for example by changing directories to the path (using + /// the corresponding environment variable): cd "$GITHUB_ACTION_PATH". For more information on environment + /// variables, + /// see + /// + /// Secure + /// use reference + /// + /// . + /// + public RawExpression GithubActionPath => field ??= new("github.action_path"); + + /// + /// For a step executing an action, this is the ref of the action being executed. For example, v2. + /// + public RawExpression GithubActionRef => field ??= new("github.action_ref"); + + /// + /// For a step executing an action, this is the owner and repository name of the action. For example, + /// actions/checkout. + /// + public RawExpression GithubActionRepository => field ??= new("github.action_repository"); + + /// + /// For a composite action, the current result of the composite action. + /// + public RawExpression GithubActionStatus => field ??= new("github.action_status"); + + /// + /// The username of the user that triggered the initial workflow run. If the workflow run is a re-run, this value may + /// differ from github.triggering_actor. Any workflow re-runs will use the privileges of github.actor, + /// even if the + /// actor initiating the re-run (github.triggering_actor) has different privileges. + /// + public RawExpression GithubActor => field ??= new("github.actor"); + + /// + /// The account ID of the person or app that triggered the initial workflow run. For example, 1234567. Note that + /// this + /// is different from the actor username. + /// + public RawExpression GithubActorId => field ??= new("github.actor_id"); + + /// + /// The URL of the GitHub REST API. + /// + public RawExpression GithubApiUrl => field ??= new("github.api_url"); + + /// + /// The base_ref or target branch of the pull request in a workflow run. This property is only available when + /// the + /// event that triggers a workflow run is either pull_request or pull_request_target. + /// + public RawExpression GithubBaseRef => field ??= new("github.base_ref"); + + /// + /// Path on the runner to the file that sets environment variables from workflow commands. This file is unique to the + /// current step and is a different file for each step in a job. + /// + public RawExpression GithubEnv => field ??= new("github.env"); + + /// + /// The full event webhook payload. You can access individual properties of the event using this context. This object + /// is identical to the webhook payload of the event that triggered the workflow run, and is different for each event. + /// + public RawExpression GithubEvent => field ??= new("github.event"); + + /// + /// The name of the event that triggered the workflow run. + /// + public RawExpression GithubEventName => field ??= new("github.event_name"); + + /// + /// The path to the file on the runner that contains the full event webhook payload. + /// + public RawExpression GithubEventPath => field ??= new("github.event_path"); + + /// + /// The URL of the GitHub GraphQL API. + /// + public RawExpression GithubGraphqlUrl => field ??= new("github.graphql_url"); + + /// + /// The head_ref or source branch of the pull request in a workflow run. This property is only available when + /// the + /// event that triggers a workflow run is either pull_request or pull_request_target. + /// + public RawExpression GithubHeadRef => field ??= new("github.head_ref"); + + /// + /// The job_id of the current job. Note: This context property is set by the Actions runner, and is only available + /// within the execution steps of a job. Otherwise, the value of this property will be null. + /// + public RawExpression GithubJob => field ??= new("github.job"); + + /// + /// Path on the runner to the file that sets system PATH variables from workflow commands. This file is unique + /// to the + /// current step and is a different file for each step in a job. + /// + public RawExpression GithubPath => field ??= new("github.path"); + + /// + /// The fully-formed ref of the branch or tag that triggered the workflow run. For branches the format is + /// refs/heads/<branch_name>. + /// For pull requests events except pull_request_target that were not merged, it is + /// refs/pull/<pr_number>/merge. + /// For tags it is refs/tags/<tag_name>. For example, refs/heads/feature-branch-1. + /// + public RawExpression GithubRef => field ??= new("github.ref"); + + /// + /// The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name + /// shown on GitHub. For example, feature-branch-1. For pull requests that were not merged, the format is + /// <pr_number>/merge. + /// + public RawExpression GithubRefName => field ??= new("github.ref_name"); + + /// + /// true if branch protections or rulesets are configured for the ref that triggered the workflow run. + /// + public RawExpression GithubRefProtected => field ??= new("github.ref_protected"); + + /// + /// The type of ref that triggered the workflow run. Valid values are branch or tag. + /// + public RawExpression GithubRefType => field ??= new("github.ref_type"); + + /// + /// The owner and repository name. For example, octocat/Hello-World. + /// + public RawExpression GithubRepository => field ??= new("github.repository"); + + /// + /// The ID of the repository. For example, 123456789. Note that this is different from the repository name. + /// + public RawExpression GithubRepositoryId => field ??= new("github.repository_id"); + + /// + /// The repository owner's username. For example, octocat. + /// + public RawExpression GithubRepositoryOwner => field ??= new("github.repository_owner"); + + /// + /// The repository owner's account ID. For example, 1234567. Note that this is different from the owner's name. + /// + public RawExpression GithubRepositoryOwnerId => field ??= new("github.repository_owner_id"); + + /// + /// The Git URL to the repository. For example, git://github.com/octocat/hello-world.git. + /// + public RawExpression GithubRepositoryUrl => field ??= new("github.repositoryUrl"); + + /// + /// The number of days that workflow run logs and artifacts are kept. + /// + public RawExpression GithubRetentionDays => field ??= new("github.retention_days"); + + /// + /// A unique number for each workflow run within a repository. This number does not change if you re-run the workflow + /// run. + /// + public RawExpression GithubRunId => field ??= new("github.run_id"); + + /// + /// A unique number for each run of a particular workflow in a repository. This number begins at 1 for the workflow's + /// first run, and increments with each new run. This number does not change if you re-run the workflow run. + /// + public RawExpression GithubRunNumber => field ??= new("github.run_number"); + + /// + /// A unique number for each attempt of a particular workflow run in a repository. This number begins at 1 for the + /// workflow run's first attempt, and increments with each re-run. + /// + public RawExpression GithubRunAttempt => field ??= new("github.run_attempt"); + + /// + /// The source of a secret used in a workflow. Possible values are None, Actions, Codespaces, or + /// Dependabot. + /// + public RawExpression GithubSecretSource => field ??= new("github.secret_source"); + + /// + /// The URL of the GitHub server. For example: https://github.com. + /// + public RawExpression GithubServerUrl => field ??= new("github.server_url"); + + /// + /// The commit SHA that triggered the workflow. The value of this commit SHA depends on the event that triggered the + /// workflow. For example, ffac537e6cbbf934b08745a378932722df287a53. + /// + public RawExpression GithubSha => field ??= new("github.sha"); + + /// + /// A token to authenticate on behalf of the GitHub App installed on your repository. This is functionally equivalent + /// to the GITHUB_TOKEN secret. Note: This context property is set by the Actions runner, and is only available + /// within the execution steps of a job. Otherwise, the value of this property will be null. + /// + public RawExpression GithubToken => field ??= new("github.token"); + + /// + /// The username of the user that initiated the workflow run. If the workflow run is a re-run, this value may differ + /// from github.actor. Any workflow re-runs will use the privileges of github.actor, even if the actor + /// initiating + /// the re-run (github.triggering_actor) has different privileges. + /// + public RawExpression GithubTriggeringActor => field ??= new("github.triggering_actor"); + + /// + /// The name of the workflow. If the workflow file doesn't specify a name, the value of this property is the + /// full + /// path of the workflow file in the repository. + /// + public RawExpression GithubWorkflow => field ??= new("github.workflow"); + + /// + /// The ref path to the workflow. For example, + /// octocat/hello-world/.github/workflows/my-workflow.yml@refs/heads/my_branch. + /// + public RawExpression GithubWorkflowRef => field ??= new("github.workflow_ref"); + + /// + /// The commit SHA for the workflow file. + /// + public RawExpression GithubWorkflowSha => field ??= new("github.workflow_sha"); + + /// + /// The default working directory on the runner for steps, and the default location of your repository when using the + /// checkout action. + /// + public RawExpression GithubWorkspace => field ??= new("github.workspace"); + + // Env + + /// + /// The env context contains variables that have been set in a workflow, job, or step. It does not contain variables + /// inherited by the runner process. + /// + public RawExpression Env => field ??= new("env"); + + // Vars + + /// + /// The vars context contains custom configuration variables set at the organization, repository, and environment + /// levels. + /// + public RawExpression Vars => field ??= new("vars"); + + // Job + + /// + /// This context changes for each job in a workflow run. You can access this context from any step in a job. + /// This object contains all the properties listed below. + /// + public RawExpression Job => field ??= new("job"); + + /// + /// The check run ID of the current job. + /// + public RawExpression JobCheckRunId => field ??= new("job.check_run_id"); + + /// + /// Information about the job's container. + /// + public RawExpression JobContainer => field ??= new("job.container"); + + /// + /// The ID of the container. + /// + public RawExpression JobContainerId => field ??= new("job.container.id"); + + /// + /// The ID of the container network. The runner creates the network used by all containers in a job. + /// + public RawExpression JobContainerNetwork => field ??= new("job.container.network"); + + /// + /// The service containers created for a job. + /// + public RawExpression JobServices => field ??= new("job.services"); + + /// + /// The current status of the job. Possible values are success, failure, or cancelled. + /// + public RawExpression JobStatus => field ??= new("job.status"); + + // Jobs (reusable workflows only) + + /// + /// This is only available in reusable workflows, and can only be used to set outputs for a reusable workflow. + /// This object contains all the properties listed below. + /// + public RawExpression Jobs => field ??= new("jobs"); + + // Steps + + /// + /// This context changes for each step in a job. You can access this context from any step in a job. + /// This object contains all the properties listed below. + /// + public RawExpression Steps => field ??= new("steps"); + + // Runner + + /// + /// This context changes for each job in a workflow run. This object contains all the properties listed below. + /// + public RawExpression Runner => field ??= new("runner"); + + /// + /// The name of the runner executing the job. This name may not be unique in a workflow run as runners at the + /// repository and organization levels could use the same name. + /// + public RawExpression RunnerName => field ??= new("runner.name"); + + /// + /// The operating system of the runner executing the job. Possible values are Linux, Windows, or + /// macOS. + /// + public RawExpression RunnerOs => field ??= new("runner.os"); + + /// + /// The architecture of the runner executing the job. Possible values are X86, X64, ARM, or + /// ARM64. + /// + public RawExpression RunnerArch => field ??= new("runner.arch"); + + /// + /// The path to a temporary directory on the runner. This directory is emptied at the beginning and end of each job. + /// Note that files will not be removed if the runner's user account does not have permission to delete them. + /// + public RawExpression RunnerTemp => field ??= new("runner.temp"); + + /// + /// The path to the directory containing preinstalled tools for GitHub-hosted runners. + /// + public RawExpression RunnerToolCache => field ??= new("runner.tool_cache"); + + /// + /// This is set only if debug logging is enabled, and always has the value of 1. It can be useful as an + /// indicator + /// to enable additional debugging or verbose logging in your own job steps. + /// + public RawExpression RunnerDebug => field ??= new("runner.debug"); + + /// + /// The environment of the runner executing the job. Possible values are: github-hosted for GitHub-hosted + /// runners + /// provided by GitHub, and self-hosted for self-hosted runners configured by the repository owner. + /// + public RawExpression RunnerEnvironment => field ??= new("runner.environment"); + + // Secrets + + /// + /// This context is the same for each job in a workflow run. You can access this context from any step in a job. + /// This object contains all the properties listed below. + /// + public RawExpression Secrets => field ??= new("secrets"); + + /// + /// Automatically created token for each workflow run. + /// + public RawExpression SecretsGithubToken => field ??= new("secrets.GITHUB_TOKEN"); + + // Strategy + + /// + /// This context changes for each job in a workflow run. You can access this context from any job or step in a + /// workflow. + /// This object contains all the properties listed below. + /// + public RawExpression Strategy => field ??= new("strategy"); + + /// + /// When this evaluates to true, all in-progress jobs are canceled if any job in a matrix fails. + /// + public RawExpression StrategyFailFast => field ??= new("strategy.fail-fast"); + + /// + /// The index of the current job in the matrix. Note: This number is a zero-based number. + /// The first job's index in the matrix is 0. + /// + public RawExpression StrategyJobIndex => field ??= new("strategy.job-index"); + + /// + /// The total number of jobs in the matrix. Note: This number is not a zero-based number. + /// For example, for a matrix with four jobs, the value of job-total is 4. + /// + public RawExpression StrategyJobTotal => field ??= new("strategy.job-total"); + + /// + /// The maximum number of jobs that can run simultaneously when using a matrix job strategy. + /// + public RawExpression StrategyMaxParallel => field ??= new("strategy.max-parallel"); + + // Matrix + + /// + /// This context is only available for jobs in a matrix, and changes for each job in a workflow run. + /// You can access this context from any job or step in a workflow. This object contains the properties listed below. + /// + public RawExpression Matrix => field ??= new("matrix"); + + // Needs + + /// + /// This context is only populated for workflow runs that have dependent jobs, and changes for each job in a workflow + /// run. + /// You can access this context from any job or step in a workflow. This object contains all the properties listed + /// below. + /// + public RawExpression Needs => field ??= new("needs"); + + // Inputs + + /// + /// This context is only available in a reusable workflow or in a workflow triggered by the workflow_dispatch + /// event. + /// You can access this context from any job or step in a workflow. This object contains the properties listed below. + /// + public RawExpression Inputs => field ??= new("inputs"); + + /// + /// The value of a specific environment variable. + /// + /// The name of the environment variable. + public RawExpression EnvVar(string envName) => + new($"env.{envName}"); + + /// + /// The value of a specific configuration variable. + /// + /// The name of the configuration variable. + public RawExpression VarsVar(string varName) => + new($"vars.{varName}"); + + /// + /// The ID of the service container. + /// + /// The service container ID. + public RawExpression JobServicesId(string serviceId) => + new($"job.services.{serviceId}.id"); + + /// + /// The ID of the service container network. The runner creates the network used by all containers in a job. + /// + /// The service container ID. + public RawExpression JobServicesNetwork(string serviceId) => + new($"job.services.{serviceId}.network"); + + /// + /// The exposed ports of the service container. + /// + /// The service container ID. + public RawExpression JobServicesPorts(string serviceId) => + new($"job.services.{serviceId}.ports"); + + /// + /// A specific exposed port of the service container. + /// + /// The service container ID. + /// The port number. + public RawExpression JobServicesPort(string serviceId, int port) => + new($"job.services.{serviceId}.ports[{port}]"); + + /// + /// The result of a job in the reusable workflow. Possible values are success, failure, cancelled, + /// or skipped. + /// + /// The job ID. + public RawExpression JobsResult(string jobId) => + new($"jobs.{jobId}.result"); + + /// + /// The set of outputs of a job in a reusable workflow. + /// + /// The job ID. + public RawExpression JobsOutputs(string jobId) => + new($"jobs.{jobId}.outputs"); + + /// + /// The value of a specific output for a job in a reusable workflow. + /// + /// The job ID. + /// The output name. + public RawExpression JobsOutput(string jobId, string outputName) => + new($"jobs.{jobId}.outputs.{outputName}"); + + /// + /// The set of outputs defined for the step. + /// + /// The step ID. + public RawExpression StepsOutputs(string stepId) => + new($"steps.{stepId}.outputs"); + + /// + /// The value of a specific output for the step. + /// + /// The step ID. + /// The output name. + public RawExpression StepsOutput(string stepId, string outputName) => + new($"steps.{stepId}.outputs.{outputName}"); + + /// + /// The result of a completed step after continue-on-error is applied. + /// Possible values are success, failure, cancelled, or skipped. + /// When a continue-on-error step fails, the outcome is failure, but the final conclusion + /// is success. + /// + /// The step ID. + public RawExpression StepsConclusion(string stepId) => + new($"steps.{stepId}.conclusion"); + + /// + /// The result of a completed step before continue-on-error is applied. + /// Possible values are success, failure, cancelled, or skipped. + /// When a continue-on-error step fails, the outcome is failure, but the final conclusion + /// is success. + /// + /// The step ID. + public RawExpression StepsOutcome(string stepId) => + new($"steps.{stepId}.outcome"); + + /// + /// The value of a specific secret. + /// + /// The name of the secret. + public RawExpression SecretsSecret(string secretName) => + new($"secrets.{secretName}"); + + /// + /// The value of a matrix property. + /// + /// The name of the matrix property. + public RawExpression MatrixProperty(string propertyName) => + new($"matrix.{propertyName}"); + + /// + /// A single job that the current job depends on. + /// + /// The job ID. + public RawExpression NeedsJob(string jobId) => + new($"needs.{jobId}"); + + /// + /// The set of outputs of a job that the current job depends on. + /// + /// The job ID. + public RawExpression NeedsOutputs(string jobId) => + new($"needs.{jobId}.outputs"); + + /// + /// The value of a specific output for a job that the current job depends on. + /// + /// The job ID. + /// The output name. + public RawExpression NeedsOutput(string jobId, string outputName) => + new($"needs.{jobId}.outputs.{outputName}"); + + /// + /// The result of a job that the current job depends on. Possible values are success, failure, + /// cancelled, or skipped. + /// + /// The job ID. + public RawExpression NeedsResult(string jobId) => + new($"needs.{jobId}.result"); + + /// + /// Each input value passed from an external workflow. + /// + /// The name of the input. + public RawExpression InputsInput(string inputName) => + new($"inputs.{inputName}"); + } + + extension(TextExpressions) + { + [PublicAPI] + public static Expressions Github => Expressions.Instance; + } +} diff --git a/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowLabelsExtensions.cs b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowLabelsExtensions.cs new file mode 100644 index 00000000..3441f1f4 --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowLabelsExtensions.cs @@ -0,0 +1,94 @@ +namespace Invex.Atom.Module.GithubWorkflows.Extensions; + +[PublicAPI] +[SuppressMessage("Performance", "CA1822:Mark members as static")] +public static class WorkflowLabelsExtensions +{ + [PublicAPI] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public sealed class RunsOn + { + // Linux x64 1x + public string Ubuntu_Slim => "ubuntu-slim"; + + // Linux x64 4x + public string Ubuntu_Latest => "ubuntu-latest"; + + public string Ubuntu_24_04 => "ubuntu-24.04"; + + public string Ubuntu_22_04 => "ubuntu-22.04"; + + // Linux ARM 4x + public string Ubuntu_24_04_Arm => "ubuntu-24.04-arm"; + + public string Ubuntu_22_04_Arm => "ubuntu-22.04-arm"; + + // Windows x64 4x + public string Windows_Latest => "windows-latest"; + + public string Windows_2025 => "windows-2025"; + + public string Windows_2022 => "windows-2022"; + + // Windows ARM 4x + public string Windows_11_Arm => "windows-11-arm"; + + // MacOS x64 4x + public string MacOs_13 => "macos-14"; + + public string MacOs_15_Intel => "macos-15-intel"; + + // MacOS ARM 3x + public string MacOs_Latest => "macos-latest"; + + public string MacOs_26 => "macos-26"; + + public string MacOs_15 => "macos-15"; + + public string MacOs_14 => "macos-14"; + + // MacOS x64 12x + public string MacOs_Latest_Large => "macos-latest-large"; + + public string MacOs_15_Large => "macos-15-large"; + + public string MacOs_14_Large => "macos-14-large"; + + public string MacOs_13_Large => "macos-13-large"; + + // MacOS ARM 5x + public string MacOs_Latest_XLarge => "macos-latest-xlarge"; + + public string MacOs_26_XLarge => "macos-26-xlarge"; + + public string MacOs_15_XLarge => "macos-15-xlarge"; + + public string MacOs_14_XLarge => "macos-14-xlarge"; + + public string MacOs_13_XLarge => "macos-13-xlarge"; + } + + [PublicAPI] + public sealed class Dependabot + { + public string NugetEcosystem => "nuget"; + + public string NugetUrl => "https://api.nuget.org/v3/index.json"; + } + + [PublicAPI] + public sealed class GithubLabels + { + internal static GithubLabels Instance => field ??= new(); + + public RunsOn RunsOn => field ??= new(); + + public Dependabot Dependabot => field ??= new(); + } + + extension(WorkflowLabels) + { + [PublicAPI] + public static GithubLabels Github => GithubLabels.Instance; + } +} diff --git a/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowOptionsExtensions.cs b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowOptionsExtensions.cs new file mode 100644 index 00000000..06271956 --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowOptionsExtensions.cs @@ -0,0 +1,271 @@ +namespace Invex.Atom.Module.GithubWorkflows.Extensions; + +[PublicAPI] +[SuppressMessage("Performance", "CA1822:Mark members as static")] +public static class WorkflowOptionsExtensions +{ + [PublicAPI] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public sealed class GithubRunsOnOptions + { + // Linux x64 1x + public GithubRunsOn Ubuntu_Slim => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.Ubuntu_Slim], + }; + + // Linux x64 4x + public GithubRunsOn Ubuntu_Latest => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.Ubuntu_Latest], + }; + + public GithubRunsOn Ubuntu_24_04 => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.Ubuntu_24_04], + }; + + public GithubRunsOn Ubuntu_22_04 => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.Ubuntu_22_04], + }; + + // Linux ARM 4x + public GithubRunsOn Ubuntu_24_04_Arm => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.Ubuntu_24_04_Arm], + }; + + public GithubRunsOn Ubuntu_22_04_Arm => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.Ubuntu_22_04_Arm], + }; + + // Windows x64 4x + public GithubRunsOn Windows_Latest => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.Windows_Latest], + }; + + public GithubRunsOn Windows_2025 => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.Windows_2025], + }; + + public GithubRunsOn Windows_2022 => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.Windows_2022], + }; + + // Windows ARM 4x + public GithubRunsOn Windows_11_Arm => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.Windows_11_Arm], + }; + + // MacOS x64 4x + public GithubRunsOn MacOs_13 => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_13], + }; + + public GithubRunsOn MacOs_15_Intel => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_15_Intel], + }; + + // MacOS ARM 3x + public GithubRunsOn MacOs_Latest => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_Latest], + }; + + public GithubRunsOn MacOs_26 => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_26], + }; + + public GithubRunsOn MacOs_15 => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_15], + }; + + public GithubRunsOn MacOs_14 => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_14], + }; + + // MacOS x64 12x + public GithubRunsOn MacOs_Latest_Large => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_Latest_Large], + }; + + public GithubRunsOn MacOs_15_Large => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_15_Large], + }; + + public GithubRunsOn MacOs_14_Large => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_14_Large], + }; + + public GithubRunsOn MacOs_13_Large => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_13_Large], + }; + + // MacOS ARM 5x + public GithubRunsOn MacOs_Latest_XLarge => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_Latest_XLarge], + }; + + public GithubRunsOn MacOs_26_XLarge => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_26_XLarge], + }; + + public GithubRunsOn MacOs_15_XLarge => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_15_XLarge], + }; + + public GithubRunsOn MacOs_14_XLarge => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_14_XLarge], + }; + + public GithubRunsOn MacOs_13_XLarge => + field ??= new() + { + Labels = [WorkflowLabels.Github.RunsOn.MacOs_13_XLarge], + }; + + public GithubRunsOn SetByMatrix { get; } = new() + { + Labels = + [ + TextExpressions + .Github + .MatrixProperty("job-runs-on") + .Evaluate(), + ], + }; + + public GithubRunsOn FromLabel(params TextExpression[] labels) => + new() + { + Labels = labels, + }; + + public GithubRunsOn FromLabel(params string[] labels) => + new() + { + Labels = labels + .Select(TextExpressions.Raw) + .ToArray(), + }; + + public GithubRunsOn FromGroup(TextExpression group) => + new() + { + Group = group, + }; + + public GithubRunsOn FromGroup(string group) => + new() + { + Group = group, + }; + + public GithubRunsOn From(TextExpression? group, TextExpression[] labels) => + new() + { + Labels = labels, + Group = group, + }; + + public GithubRunsOn From(string group, string[] labels) => + new() + { + Labels = labels + .Select(TextExpressions.Raw) + .ToArray(), + Group = group, + }; + } + + [PublicAPI] + public sealed class GithubTokenPermissionsOptions + { + public GithubTokenPermissionsOption NoneAll => new(new Permissions.All(PermissionsLevel.None)); + + public GithubTokenPermissionsOption ReadAll => new(new Permissions.All(PermissionsLevel.Read)); + + public GithubTokenPermissionsOption WriteAll => new(new Permissions.All(PermissionsLevel.Write)); + + public GithubTokenPermissionsOption Set(Permissions permissions) => + new(permissions); + } + + [PublicAPI] + public sealed class GithubDependabotOptions + { + public DependabotConfigOption Configure( + StructuredText.GithubActions.DependabotConfigModel.Model.DependabotConfig config) => + new(config); + } + + [PublicAPI] + public sealed class GithubStepsOptions + { + public GithubCheckoutStep Checkout(GithubCheckoutStep step) => + step; + } + + [PublicAPI] + public sealed class GithubOptions + { + internal static GithubOptions Instance => field ??= new(); + + public GithubRunsOnOptions RunsOn => field ??= new(); + + public GithubTokenPermissionsOptions TokenPermissions => field ??= new(); + + public GithubDependabotOptions Dependabot => field ??= new(); + + public GithubStepsOptions Steps => field ??= new(); + } + + extension(BuildOptions) + { + [PublicAPI] + public static GithubOptions Github => GithubOptions.Instance; + } +} diff --git a/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowPresetsExtensions.cs b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowPresetsExtensions.cs new file mode 100644 index 00000000..1320b609 --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowPresetsExtensions.cs @@ -0,0 +1,26 @@ +namespace Invex.Atom.Module.GithubWorkflows.Extensions; + +[PublicAPI] +[SuppressMessage("Performance", "CA1822:Mark members as static")] +public static class WorkflowPresetsExtensions +{ + [PublicAPI] + public sealed class Presets + { + internal static Presets Instance => field ??= new(); + + public WorkflowDefinition Dependabot( + StructuredText.GithubActions.DependabotConfigModel.Model.DependabotConfig config) => + new("dependabot") + { + Options = [new DependabotConfigOption(config)], + Types = [WorkflowTypes.Github.Dependabot], + }; + } + + extension(WorkflowPresets) + { + [PublicAPI] + public static Presets Github => Presets.Instance; + } +} diff --git a/DecSm.Atom.Module.GithubWorkflows/Extensions.cs b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowTargetDefinitionExtensions.cs similarity index 51% rename from DecSm.Atom.Module.GithubWorkflows/Extensions.cs rename to src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowTargetDefinitionExtensions.cs index 4d4af9f8..cd6f32c5 100644 --- a/DecSm.Atom.Module.GithubWorkflows/Extensions.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowTargetDefinitionExtensions.cs @@ -1,11 +1,11 @@ -namespace DecSm.Atom.Module.GithubWorkflows; +namespace Invex.Atom.Module.GithubWorkflows.Extensions; /// /// Provides extension methods for to simplify GitHub Actions workflow /// configuration. /// [PublicAPI] -public static class Extensions +public static class WorkflowTargetDefinitionExtensions { /// /// Extension methods for . @@ -19,52 +19,48 @@ public static class Extensions /// The modified for chaining. /// /// This method sets up a matrix dimension for the `JobRunsOn` parameter, allowing the job - /// to execute on multiple runner environments. It also adds the - /// option to indicate that the runner is determined by the matrix. + /// to execute on multiple runner environments. It also adds the + /// + /// option to indicate that the matrix determines the runner. /// [PublicAPI] - public WorkflowTargetDefinition WithGithubRunnerMatrix(string[] labels) => + public WorkflowTargetDefinition WithGithubRunsOnMatrix(IEnumerable labels) => workflowTargetDefinition .WithMatrixDimensions(new MatrixDimension(nameof(IJobRunsOn.JobRunsOn)) { - Values = labels, + Values = labels.ToList(), }) - .WithOptions(GithubRunsOn.SetByMatrix); + .WithOptions(BuildOptions.Github.RunsOn.SetByMatrix); /// - /// Configures the workflow target to inject the GitHub token as a secret. + /// Configures the workflow target to run on a matrix of GitHub Actions runner labels. /// + /// An array of runner labels (e.g., "ubuntu-latest", "windows-latest"). /// The modified for chaining. /// - /// This method adds a option for the , - /// making the GitHub Actions token available as a secret within the workflow job. + /// This method sets up a matrix dimension for the `JobRunsOn` parameter, allowing the job + /// to execute on multiple runner environments. It also adds the + /// + /// option to indicate that the matrix determines the runner. /// [PublicAPI] - public WorkflowTargetDefinition WithGithubTokenInjection(GithubTokenPermissionsOption? permissions = null) => - permissions is not null - ? workflowTargetDefinition.WithOptions( - WorkflowSecretInjection.Create(nameof(IGithubHelper.GithubToken)), - permissions) - : workflowTargetDefinition.WithOptions( - WorkflowSecretInjection.Create(nameof(IGithubHelper.GithubToken))); + public WorkflowTargetDefinition WithGithubRunsOnMatrix(IEnumerable labels) => + workflowTargetDefinition.WithGithubRunsOnMatrix( + labels.Select(StructuredText.Expressions.WorkflowExpressionExtensions.Raw)); /// - /// Configures the workflow target to set specific permissions for the injected GitHub token. + /// Configures the workflow target to inject the GitHub token as a secret. /// - /// The permissions to assign to the GitHub Actions token for this workflow job. /// The modified for chaining. /// - /// This method adds a to the workflow target, allowing you to - /// specify fine-grained permissions for the GitHub Actions token used in the job. - /// - /// Example: - /// - /// target.WithGithubTokenPermissions(GithubTokenPermissionsOption.ReadPackages); - /// - /// + /// This method adds a option for the , + /// making the GitHub Actions token available as a secret within the workflow job. /// [PublicAPI] - public WorkflowTargetDefinition WithGithubTokenPermissions(GithubTokenPermissionsOption permissions) => - workflowTargetDefinition.WithOptions(permissions); + public WorkflowTargetDefinition WithGithubTokenInjection(Permissions? permissions = null) => + permissions is null + ? workflowTargetDefinition.WithOptions(BuildOptions.Inject.Secret(nameof(IGithubHelper.GithubToken))) + : workflowTargetDefinition.WithOptions(BuildOptions.Inject.Secret(nameof(IGithubHelper.GithubToken)), + new GithubTokenPermissionsOption(permissions)); } } diff --git a/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowTriggersExtensions.cs b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowTriggersExtensions.cs new file mode 100644 index 00000000..54cdb248 --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowTriggersExtensions.cs @@ -0,0 +1,24 @@ +namespace Invex.Atom.Module.GithubWorkflows.Extensions; + +[PublicAPI] +[SuppressMessage("Performance", "CA1822:Mark members as static")] +public static class WorkflowTriggersExtensions +{ + [PublicAPI] + public sealed class Triggers + { + internal static Triggers Instance { get; } = new(); + + public GithubTrigger OnRelease(On.Release.ReleaseType type = On.Release.ReleaseType.released) => + new(new On.Release([type])); + + public GithubTrigger OnSchedule(params IReadOnlyList crons) => + new(new On.Schedule(crons)); + } + + extension(WorkflowTriggers) + { + [PublicAPI] + public static Triggers Github => Triggers.Instance; + } +} diff --git a/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowTypesExtensions.cs b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowTypesExtensions.cs new file mode 100644 index 00000000..0390b17b --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Extensions/WorkflowTypesExtensions.cs @@ -0,0 +1,22 @@ +namespace Invex.Atom.Module.GithubWorkflows.Extensions; + +[PublicAPI] +[SuppressMessage("Performance", "CA1822:Mark members as static")] +public static class WorkflowTypesExtensions +{ + [PublicAPI] + public sealed class Types + { + internal static Types Instance => field ??= new(); + + public GithubWorkflowType Action => field ??= new(); + + public DependabotWorkflowType Dependabot => field ??= new(); + } + + extension(WorkflowTypes) + { + [PublicAPI] + public static Types Github => Types.Instance; + } +} diff --git a/DecSm.Atom.Module.GithubWorkflows/Github.cs b/src/Invex.Atom.Module.GithubWorkflows/Github.cs similarity index 91% rename from DecSm.Atom.Module.GithubWorkflows/Github.cs rename to src/Invex.Atom.Module.GithubWorkflows/Github.cs index 7215cd9c..d223fa17 100644 --- a/DecSm.Atom.Module.GithubWorkflows/Github.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/Github.cs @@ -1,4 +1,6 @@ -namespace DecSm.Atom.Module.GithubWorkflows; +using Environment = System.Environment; + +namespace Invex.Atom.Module.GithubWorkflows; /// /// Provides utility methods and properties for interacting with GitHub Actions workflows. @@ -16,64 +18,6 @@ public static class Github /// public static bool IsGithubActions => Variables.Actions.Equals("true", StringComparison.CurrentCultureIgnoreCase); - /// - /// Gets the default pipeline publish directory path for GitHub Actions. - /// - /// - /// This path is typically used for storing artifacts that are intended to be uploaded - /// to GitHub Actions artifact storage. - /// - public static string PipelinePublishDirectory => "${{ github.workspace }}/.github/publish"; - - /// - /// Gets the default pipeline artifact directory path for GitHub Actions. - /// - /// - /// This path is typically used for downloading artifacts from GitHub Actions artifact storage. - /// - public static string PipelineArtifactDirectory => "${{ github.workspace }}/.github/artifacts"; - - /// - /// Gets an instance of for defining GitHub-specific workflow types. - /// - public static GithubWorkflowType WorkflowType { get; } = new(); - - /// - /// Creates a default Dependabot workflow definition with NuGet registry and update group. - /// - /// A pre-configured for Dependabot with NuGet settings. - public static WorkflowDefinition DependabotDefaultWorkflow() => - DependabotWorkflow(new() - { - Registries = [new("nuget", DependabotValues.NugetType, DependabotValues.NugetUrl)], - Updates = - [ - new(DependabotValues.NugetEcosystem) - { - Registries = ["nuget"], - Groups = - [ - new("nuget-deps") - { - Patterns = ["*"], - }, - ], - }, - ], - }); - - /// - /// Creates a Dependabot workflow definition with custom options. - /// - /// The custom Dependabot options to apply. - /// A configured with the provided Dependabot options. - public static WorkflowDefinition DependabotWorkflow(DependabotOptions dependabotOptions) => - new("dependabot") - { - Options = [dependabotOptions], - WorkflowTypes = [new DependabotWorkflowType()], - }; - /// /// Contains constant strings for all known GitHub Actions environment variable names. /// diff --git a/src/Invex.Atom.Module.GithubWorkflows/GithubActions/GithubWorkflowFileWriter.cs b/src/Invex.Atom.Module.GithubWorkflows/GithubActions/GithubWorkflowFileWriter.cs new file mode 100644 index 00000000..62701ab9 --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/GithubActions/GithubWorkflowFileWriter.cs @@ -0,0 +1,763 @@ +namespace Invex.Atom.Module.GithubWorkflows.GithubActions; + +internal sealed class GithubWorkflowFileWriter( + IBuildDefinition buildDefinition, + BuildModel buildModel, + IParamService paramService, + IRootedFileSystem fileSystem, + AtomProjectData atomProjectData, + ILogger logger +) : WorkflowFileWriter(fileSystem, logger) +{ + private readonly IRootedFileSystem _fileSystem = fileSystem; + + protected override string FileExtension => "yml"; + + protected override RootedPath FileLocation => _fileSystem.AtomRootDirectory / ".github" / "workflows"; + + protected override string WriteWorkflow(WorkflowModel workflow) + { + var resolvedWorkflow = BuildWorkflow(workflow); + + var actionWriter = new GithubActionWriter(); + actionWriter.Write(resolvedWorkflow); + + return actionWriter.TextWriter.ToString(); + } + + private GithubAction BuildWorkflow(WorkflowModel workflow) => + new() + { + Name = workflow.Name, + On = BuildTriggers(workflow), + Jobs = workflow + .Jobs + .Select(x => BuildJob(workflow, x)) + .ToList(), + Permissions = BuildPermissions(GithubTokenPermissionsOption.Get(workflow.Options) ?? + BuildOptions.Github.TokenPermissions.NoneAll), + }; + + private List BuildTriggers(WorkflowModel workflow) => + workflow + .Triggers + .Select(trigger => trigger switch + { + GithubTrigger githubTrigger => githubTrigger.On, + GitPullRequestTrigger gitPullRequestTrigger => new On.PullRequest(gitPullRequestTrigger + .Types + .Select(type => Enum.TryParse(type, out var result) + ? result + : throw new ArgumentOutOfRangeException(nameof(type))) + .ToList()) + { + Branches = gitPullRequestTrigger.IncludedBranches, + BranchesIgnore = gitPullRequestTrigger.ExcludedBranches, + Tags = null, + TagsIgnore = null, + Paths = gitPullRequestTrigger.IncludedPaths, + PathsIgnore = gitPullRequestTrigger.ExcludedPaths, + }, + + GitPushTrigger gitPushTrigger => new On.Push + { + Branches = gitPushTrigger.IncludedBranches, + BranchesIgnore = gitPushTrigger.ExcludedBranches, + Tags = gitPushTrigger.IncludedTags, + TagsIgnore = gitPushTrigger.ExcludedTags, + Paths = gitPushTrigger.IncludedPaths, + PathsIgnore = gitPushTrigger.ExcludedPaths, + }, + + ManualTrigger manualTrigger => new On.WorkflowDispatch(manualTrigger + .Inputs + ?.Select(input => + { + var inputParamName = buildDefinition.ParamDefinitions + .FirstOrDefault(x => x.Value.ArgName == input.Name) + .Key; + + if (inputParamName is null) + throw new InvalidOperationException( + $"Workflow {workflow.Name} has a manual trigger input named {input.Name} that does not correspond to any parameter in the build definition"); + + switch (input) + { + case ManualBoolInput boolInput: + { + bool? defaultBoolValue = null; + + if (boolInput.DefaultValue.HasValue) + defaultBoolValue = boolInput.DefaultValue.Value; + else + using (paramService.CreateDefaultValuesOnlyScope()) + switch (buildDefinition.AccessParam(inputParamName)) + { + case bool boolParam: + { + defaultBoolValue = boolParam; + + break; + } + + case string stringParam: + { + if (bool.TryParse(stringParam, out var parsedBool)) + defaultBoolValue = parsedBool; + + break; + } + } + + return new WorkflowDispatchInput.Boolean + { + Name = input.Name, + Description = input.Description, + Required = input.Required ?? defaultBoolValue is null, + Default = defaultBoolValue switch + { + true => "true", + false => "false", + null => null, + }, + }; + } + + case ManualChoiceInput choiceInput: + { + using (paramService.CreateDefaultValuesOnlyScope()) + { + var defaultChoiceValue = choiceInput.DefaultValue is { Length: > 0 } + ? choiceInput.DefaultValue + : buildDefinition + .AccessParam(inputParamName) + ?.ToString(); + + return new WorkflowDispatchInput.Choice + { + Name = input.Name, + Description = input.Description, + Required = input.Required ?? defaultChoiceValue is not { Length: > 0 }, + Default = defaultChoiceValue, + Options = choiceInput.Choices, + }; + } + } + + case ManualStringInput stringInput: + { + using (paramService.CreateDefaultValuesOnlyScope()) + { + var defaultStringValue = stringInput.DefaultValue is { Length: > 0 } + ? stringInput.DefaultValue + : buildDefinition + .AccessParam(inputParamName) + ?.ToString(); + + return new WorkflowDispatchInput.String + { + Name = stringInput.Name, + Description = stringInput.Description, + Required = input.Required ?? defaultStringValue is not { Length: > 0 }, + Default = defaultStringValue, + }; + } + } + + default: + { + throw new ArgumentOutOfRangeException(nameof(input)); + } + } + }) + .ToList() ?? []), + _ => throw new ArgumentOutOfRangeException(nameof(trigger), trigger, null), + }) + .ToList(); + + private Job BuildJob(WorkflowModel workflow, WorkflowJobModel job) => + new() + { + Name = job.Name, + Permissions = BuildPermissions(GithubTokenPermissionsOption.Get(job.TargetStep.Options)), + Needs = job + .JobDependencies + .Distinct() + .ToList(), + If = TargetCondition + .GetOptions(job.TargetStep.Options) + .ToList() switch + { + { Count: > 1 } options => options[0] + .Condition + .And(options + .Skip(1) + .Select(x => x.Condition) + .ToArray()), + { Count: 1 } option => option[0].Condition, + _ => null, + }, + RunsOn = GithubRunsOn.Get(job.TargetStep.Options) is { } runsOn + ? new() + { + Labels = runsOn.Labels, + Group = runsOn.Group, + } + : new() + { + Labels = ["ubuntu-latest"], + }, + Snapshot = null, + Environment = DeployToEnvironment.Get(job.TargetStep.Options) is { } environment + ? new() + { + Name = environment.EnvironmentName, + } + : null, + Concurrency = null, + Outputs = buildModel.GetTarget(job.TargetStep.Name) + .ProducedVariables is { Count: > 0 } outputs + ? outputs.ToDictionary(x => buildDefinition.ParamDefinitions[x].ArgName, + x => TextExpressions + .Raw("steps")[job.Name]["outputs"][buildDefinition.ParamDefinitions[x].ArgName] + .Evaluate()) + : null, + Env = null, + Strategy = job.TargetStep.MatrixDimensions.Count > 0 + ? new() + { + Matrix = new() + { + Map = job + .TargetStep + .MatrixDimensions + // TODO: Proper duplicate detection + .Distinct() + .ToDictionary(x => buildDefinition.ParamDefinitions[x.Name].ArgName, x => x.Values), + }, + } + : null, + ContinueOnError = null, + Container = null, + Services = null, + Steps = BuildSteps(workflow, job), + }; + + private static Permissions? BuildPermissions(GithubTokenPermissionsOption? permissionOption) => + permissionOption?.Permissions switch + { + Permissions.All all => all, + Permissions.Exact exact => exact.Permissions.IsAll(PermissionsLevel.Write) + ? new Permissions.All(PermissionsLevel.Write) + : exact.Permissions.IsAll(PermissionsLevel.Read) + ? new Permissions.All(PermissionsLevel.Read) + : exact.Permissions.IsAll(PermissionsLevel.None) + ? new Permissions.All(PermissionsLevel.None) + : exact.Permissions, + null => null, + _ => throw new ArgumentOutOfRangeException(nameof(permissionOption), permissionOption, null), + }; + + private List BuildSteps(WorkflowModel workflow, WorkflowJobModel job) + { + var additionalSteps = IAdditionalStepOption + .GetOptions(job.TargetStep.Options) + .ToList(); + + // Special case: Add default checkout step if there isn't one + if (!additionalSteps.Any(x => x is CheckoutStep)) + additionalSteps.Add(new GithubCheckoutStep + { + Enabled = true, + FetchDepth = TextExpressions.From(0), + }); + + additionalSteps = additionalSteps.ToList(); + + // Add pre-target additional steps + var steps = new List(additionalSteps + .Where(x => x.Order < 0) + .OrderBy(x => x.Order) + .Select(BuildAdditionalStep) + .OfType()); + + var matrixParams = job + .TargetStep + .MatrixDimensions + .Select(dimension => buildDefinition.ParamDefinitions[dimension.Name].ArgName) + .Select(name => (Name: name, Value: (TextExpression)TextExpressions + .Raw("matrix")[name] + .Evaluate())) + .ToList(); + + var buildSliceValue = matrixParams switch + { + { Count: 0 } => null, + { Count: 1 } => matrixParams[0].Value, + { Count: > 1 } => new ConcatExpression(matrixParams + .Select((x, i) => i == matrixParams.Count - 1 + ? x.Value + : new ConcatExpression([x.Value, "-"])) + .ToArray()), + }; + + if (buildSliceValue is not null) + matrixParams.Add(new("build-slice", buildSliceValue)); + + var target = buildModel.GetTarget(job.TargetStep.Name); + + if (target.ConsumedArtifacts.Count > 0) + { + foreach (var consumedArtifact in target.ConsumedArtifacts) + { + var consumedStep = workflow + .Jobs + .Select(x => x.TargetStep) + .Single(x => x.Name == consumedArtifact.TargetName); + + if (SuppressArtifactPublishingOption.Get(consumedStep.Options) is { Enabled: true }) + logger.LogWarning( + "Workflow {WorkflowName} target {TargetName} consumes artifact {ArtifactName} from target {SourceTargetName}, which has artifact publishing suppressed; this may cause the workflow to fail", + workflow.Name, + job.TargetStep.Name, + consumedArtifact.ArtifactName, + consumedArtifact.TargetName); + } + + if (UseCustomArtifactProvider.Get(job.TargetStep.Options) is { Enabled: true }) + foreach (var slice in target.ConsumedArtifacts.GroupBy(a => a.BuildSlice)) + { + var artifactNames = slice + .AsEnumerable() + .Select(x => x.ArtifactName) + .ToArray(); + + var retrieveTarget = buildModel.GetTarget(nameof(IRetrieveArtifact.RetrieveArtifact)); + + var retrieveStepEnv = BuildTargetStepEnv(workflow, + job, + retrieveTarget.Params, + retrieveTarget.ConsumedVariables, + matrixParams); + + retrieveStepEnv["atom-artifacts"] = string.Join(",", artifactNames); + + if (!retrieveStepEnv.ContainsKey("build-slice")) + if (slice.Key is { Length: > 0 }) + retrieveStepEnv.Add("build-slice", slice.Key); + else if (buildSliceValue is not null) + retrieveStepEnv.Add("build-slice", buildSliceValue); + + var retrieveArtifactsName = artifactNames switch + { + [var artifactName] => $"Retrieve artifact `{artifactName}`", + _ => artifactNames.Length < 60 + ? $"Retrieve artifacts `{string.Join(", ", artifactNames)}`" + : "Retrieve multiple artifacts", + }; + + if (atomProjectData.IsFileBasedApp) + { + if (AppContext.GetData("EntryPointFilePath") is not string fileName) + throw new InvalidOperationException( + "AtomFileSystem reports file-based app but AppContext.EntryPointFilePath is null, cannot determine file path to run"); + + var filePathRelativeToRoot = + _fileSystem.FileSystem.Path.GetRelativePath(_fileSystem.AtomRootDirectory, fileName); + + steps.Add(new Step.RunStep + { + Name = retrieveArtifactsName, + Run = $"dotnet run --file {filePathRelativeToRoot} -- RetrieveArtifact --skip --headless", + Env = retrieveStepEnv, + }); + } + else + { + var projectPath = FindProjectPath(_fileSystem, atomProjectData.ProjectName); + + steps.Add(new Step.RunStep + { + Name = retrieveArtifactsName, + Run = $"dotnet run --project {projectPath} -- RetrieveArtifact --skip --headless", + Env = retrieveStepEnv, + }); + } + } + else + steps.AddRange(target.ConsumedArtifacts.Select(artifact => new Step.UsesStep + { + Name = $"Retrieve {artifact.ArtifactName}", + Uses = "actions/download-artifact@v8", + With = new Dictionary + { + ["name"] = artifact.BuildSlice is { Length: > 0 } + ? new ConcatExpression([artifact.ArtifactName, "-", artifact.BuildSlice]) + : buildSliceValue is not null + ? new ConcatExpression([artifact.ArtifactName, "-", buildSliceValue]) + : artifact.ArtifactName, + ["path"] = $"${{{{ github.workspace }}}}/.github/artifacts/{artifact.ArtifactName}", + }, + })); + } + + var targetStepCondition = TargetStepCondition + .GetOptions(job.TargetStep.Options) + .ToList() switch + { + { Count: > 1 } multiple => new AndExpression(multiple + .Select(x => x.Condition) + .ToArray()), + { Count: 1 } single => single[0].Condition, + _ => null, + }; + + var targetStepEnv = BuildTargetStepEnv(workflow, job, target.Params, target.ConsumedVariables, matrixParams); + + if (atomProjectData.IsFileBasedApp) + { + if (AppContext.GetData("EntryPointFilePath") is not string fileName) + throw new InvalidOperationException( + "AtomFileSystem reports file-based app but AppContext.EntryPointFilePath is null, cannot determine file path to run"); + + var filePathRelativeToRoot = + _fileSystem.FileSystem.Path.GetRelativePath(_fileSystem.AtomRootDirectory, fileName); + + steps.Add(new Step.RunStep + { + Id = job.TargetStep.Name, + Name = job.TargetStep.Name, + If = targetStepCondition, + Run = $"dotnet run --file {filePathRelativeToRoot} -- {job.TargetStep.Name} --skip --headless", + Env = targetStepEnv, + }); + } + else + { + var projectPath = FindProjectPath(_fileSystem, atomProjectData.ProjectName); + + steps.Add(new Step.RunStep + { + Id = job.TargetStep.Name, + Name = job.TargetStep.Name, + If = targetStepCondition, + Run = $"dotnet run --project {projectPath} -- {job.TargetStep.Name} --skip --headless", + Env = targetStepEnv, + }); + } + + if (target.ProducedArtifacts.Count > 0 && !SuppressArtifactPublishingOption.IsEnabled(job.TargetStep.Options)) + { + if (UseCustomArtifactProvider.Get(job.TargetStep.Options) is { Enabled: true }) + foreach (var slice in target.ProducedArtifacts.GroupBy(a => a.BuildSlice)) + { + var artifactNames = slice + .AsEnumerable() + .Select(x => x.ArtifactName) + .ToArray(); + + var storeTarget = buildModel.GetTarget(nameof(IRetrieveArtifact.RetrieveArtifact)); + + var storeStepEnv = BuildTargetStepEnv(workflow, + job, + storeTarget.Params, + storeTarget.ConsumedVariables, + matrixParams); + + storeStepEnv["atom-artifacts"] = string.Join(",", artifactNames); + + if (!storeStepEnv.ContainsKey("build-slice")) + if (slice.Key is { Length: > 0 }) + storeStepEnv.Add("build-slice", slice.Key); + else if (buildSliceValue is not null) + storeStepEnv.Add("build-slice", buildSliceValue); + + var storeArtifactsName = artifactNames switch + { + [var artifactName] => $"Store artifact `{artifactName}`", + _ => artifactNames.Length < 60 + ? $"Store artifacts `{string.Join(", ", artifactNames)}`" + : "Store multiple artifacts", + }; + + if (atomProjectData.IsFileBasedApp) + { + if (AppContext.GetData("EntryPointFilePath") is not string fileName) + throw new InvalidOperationException( + "AtomFileSystem reports file-based app but AppContext.EntryPointFilePath is null, cannot determine file path to run"); + + var filePathRelativeToRoot = + _fileSystem.FileSystem.Path.GetRelativePath(_fileSystem.AtomRootDirectory, fileName); + + steps.Add(new Step.RunStep + { + Name = storeArtifactsName, + Run = $"dotnet run --file {filePathRelativeToRoot} -- StoreArtifact --skip --headless", + Env = storeStepEnv, + }); + } + else + { + var projectPath = FindProjectPath(_fileSystem, atomProjectData.ProjectName); + + steps.Add(new Step.RunStep + { + Name = storeArtifactsName, + Run = $"dotnet run --project {projectPath} -- StoreArtifact --skip --headless", + Env = storeStepEnv, + }); + } + } + else + steps.AddRange(target.ProducedArtifacts.Select(artifact => new Step.UsesStep + { + Name = $"Store {artifact.ArtifactName}", + Uses = "actions/upload-artifact@v7", + With = new Dictionary + { + ["name"] = artifact.BuildSlice is { Length: > 0 } + ? new ConcatExpression([artifact.ArtifactName, "-", artifact.BuildSlice]) + : buildSliceValue is not null + ? new ConcatExpression([artifact.ArtifactName, "-", buildSliceValue]) + : artifact.ArtifactName, + ["path"] = $"${{{{ github.workspace }}}}/.github/publish/{artifact.ArtifactName}", + }, + })); + } + + steps.AddRange(additionalSteps + .Where(x => x.Order > 0) + .OrderBy(x => x.Order) + .Select(BuildAdditionalStep) + .OfType()); + + return steps; + } + + private static Step? BuildAdditionalStep(IAdditionalStepOption additionalStep) => + additionalStep switch + { + IGithubAdditionalStepOption { Enabled: true } githubStep => githubStep.Build(), + IGithubAdditionalStepOption => null, + SetupDotnetStep setupDotnetStep => BuildSetupDotnetStep(setupDotnetStep), + AddNugetFeedsStep addNugetFeedsStep => BuildAddNugetFeedsStep(addNugetFeedsStep), + _ => throw new InvalidOperationException( + $"Unknown additional step type: {additionalStep.GetType().FullName}"), + }; + + private static Step.UsesStep BuildSetupDotnetStep(SetupDotnetStep step) + { + var with = new Dictionary(); + + if (step.DotnetVersion is not null) + { + with.Add("dotnet-version", step.DotnetVersion); + + if (step.Quality is not null) + with.Add("quality", step.Quality.ToString()!.ToLowerInvariant()); + } + + return new() + { + Name = step.DotnetVersion is not null + ? TextExpressions.Concat(["Setup .NET ", step.DotnetVersion]) + : "Setup .NET", + Uses = "actions/setup-dotnet@v5", + With = with.Count > 0 + ? with + : null, + }; + } + + private static Step.RunStep BuildAddNugetFeedsStep(AddNugetFeedsStep step) + { + var feedsToAdd = step.FeedsToAdd.ToList(); + + var toolVersion = ""; + + if (step.SyncAtomToolVersionToLibraryVersion) + { + if (SemVer.TryParse(typeof(AtomHost).Assembly.GetCustomAttribute() + ?.InformationalVersion ?? + "", + out var semVer)) + toolVersion = + SemVer.Parse($"{semVer.Prefix}{(semVer.IsPreRelease ? $"-{semVer.PreRelease}" : string.Empty)}"); + else + throw new InvalidOperationException( + "Failed to parse Invex.Atom.Host assembly version as SemVer for syncing atom tool version"); + } + + // If we are using .net 10+ then we can use the dotnet tool exec command instead of installing the tool to run it + if (SemVer.TryParse(RuntimeInformation + .FrameworkDescription + .Replace(".NET ", "") + .Replace("x", "0"), + out var version) && + version.Major >= 10) + return new() + { + Name = "Setup NuGet", + Shell = "bash", + Env = feedsToAdd.ToDictionary( + k => AddNugetFeedsStep.GetEnvVarNameForFeed(k.FeedName), + v => TextExpressions + .Raw("secrets")[v.SecretName] + .Evaluate()), + Run = string.Join("\n", + feedsToAdd.Select(feedToAdd => step.SyncAtomToolVersionToLibraryVersion + ? $"dotnet tool exec invex.atom.tool@{toolVersion} -y -- nuget-add --name \"{feedToAdd.FeedName}\" --url \"{feedToAdd.FeedUrl}\"" + : $"dotnet tool exec invex.atom.tool -y -- nuget-add --name \"{feedToAdd.FeedName}\" --url \"{feedToAdd.FeedUrl}\"")), + }; + + return new() + { + Name = "Setup NuGet", + Shell = "bash", + Env = feedsToAdd.ToDictionary( + k => AddNugetFeedsStep.GetEnvVarNameForFeed(k.FeedName), + v => TextExpressions + .Raw("secrets")[v.SecretName] + .Evaluate()), + Run = string.Join("\n", + feedsToAdd + .Select(feedToAdd => step.SyncAtomToolVersionToLibraryVersion + ? $"atom nuget-add --name \"{feedToAdd.FeedName}\" --url \"{feedToAdd.FeedUrl}\" --tool-version \"{toolVersion}\"" + : $"atom nuget-add --name \"{feedToAdd.FeedName}\" --url \"{feedToAdd.FeedUrl}\"") + .Prepend("dotnet tool update --global Invex.Atom.Tool")), + }; + } + + private Dictionary BuildTargetStepEnv( + WorkflowModel workflow, + WorkflowJobModel job, + IReadOnlyList usedParams, + IReadOnlyList consumedVariables, + List<(string Name, TextExpression Value)> matrixParams) + { + var targetStepEnv = new Dictionary(); + + foreach (var githubManualTrigger in workflow.Triggers.OfType()) + foreach (var input in githubManualTrigger.Inputs?.Where(i => usedParams + .Select(p => p.Param.ArgName) + .Any(p => p == i.Name)) ?? []) + targetStepEnv[input.Name] = TextExpressions + .Raw("inputs")[input.Name] + .Evaluate(); + + foreach (var consumedVariable in consumedVariables) + targetStepEnv[buildDefinition.ParamDefinitions[consumedVariable.VariableName].ArgName] = TextExpressions + .Raw("needs")[consumedVariable.TargetName]["outputs"][buildDefinition + .ParamDefinitions[consumedVariable.VariableName].ArgName] + .Evaluate(); + + var requiredSecrets = usedParams + .Where(x => x.Param.IsSecret) + .Select(x => x) + .ToArray(); + + if (requiredSecrets.Any(x => x.Param.IsSecret)) + { + foreach (var injectedSecret in WorkflowSecretInjectionForSecretProvider.GetOptions(workflow.Options)) + if (buildDefinition.ParamDefinitions.GetValueOrDefault(injectedSecret.SecretName) is + { } paramDefinition) + targetStepEnv[paramDefinition.ArgName] = TextExpressions + .Raw("secrets")[paramDefinition.EnvVarName] + .Evaluate(); + + foreach (var injectedEvVar in WorkflowSecretsInjectionFromEnvironment.GetOptions(workflow.Options)) + if (buildDefinition.ParamDefinitions.GetValueOrDefault(injectedEvVar.SecretName) is { } paramDefinition) + targetStepEnv[paramDefinition.ArgName] = TextExpressions + .Raw("vars")[paramDefinition.EnvVarName] + .Evaluate(); + } + + foreach (var requiredSecret in requiredSecrets) + if (WorkflowSecretInjection + .GetOptions(job.TargetStep.Options) + .Any(x => x.Value == requiredSecret.Param.Name)) + targetStepEnv[requiredSecret.Param.ArgName] = TextExpressions + .Raw("secrets")[requiredSecret.Param.EnvVarName] + .Evaluate(); + + var environmentInjections = WorkflowParamInjectionFromEnvironment.GetOptions(job.TargetStep.Options); + var paramInjections = WorkflowParamInjection.GetOptions(job.TargetStep.Options); + var environmentVariableInjections = WorkflowEnvironmentVariableInjection.GetOptions(job.TargetStep.Options); + + environmentInjections = environmentInjections + .Where(e => paramInjections.All(p => p.Name != e.Value)) + .ToList(); + + foreach (var environmentInjection in environmentInjections) + { + if (!buildDefinition.ParamDefinitions.TryGetValue(environmentInjection.Value, out var paramDefinition)) + { + logger.LogWarning( + "Workflow {WorkflowName} command {CommandName} has an injection for parameter {ParamName} that does not exist", + workflow.Name, + job.TargetStep.Name, + environmentInjection.Value); + + continue; + } + + targetStepEnv[paramDefinition.ArgName] = TextExpressions + .Raw("vars")[paramDefinition.EnvVarName] + .Evaluate(); + } + + foreach (var paramInjection in paramInjections) + { + if (!buildDefinition.ParamDefinitions.TryGetValue(paramInjection.Name, out var paramDefinition)) + { + logger.LogWarning( + "Workflow {WorkflowName} command {CommandName} has an injection for parameter {ParamName} that is not consumed by the command", + workflow.Name, + job.TargetStep.Name, + paramInjection.Name); + + continue; + } + + targetStepEnv[paramDefinition.ArgName] = paramInjection.InjectionExpression is EvaluateExpression + ? paramInjection.InjectionExpression + : paramInjection.InjectionExpression.Evaluate(); + } + + foreach (var environmentVariableInjection in environmentVariableInjections) + targetStepEnv[environmentVariableInjection.Name] = environmentVariableInjection.Value is EvaluateExpression + ? environmentVariableInjection.Value + : environmentVariableInjection.Value.Evaluate(); + + foreach (var matrixParam in matrixParams) + targetStepEnv[matrixParam.Name] = matrixParam.Value; + + return targetStepEnv; + } + + private static string FindProjectPath(IRootedFileSystem fileSystem, string projectName) + { + var projectPath = fileSystem + .FileSystem + .DirectoryInfo + .New(fileSystem.AtomRootDirectory) + .EnumerateFiles("*.csproj", + new EnumerationOptions + { + IgnoreInaccessible = true, + MaxRecursionDepth = 4, + RecurseSubdirectories = true, + ReturnSpecialDirectories = false, + }) + .FirstOrDefault(f => f.Name.Equals($"{projectName}.csproj", StringComparison.OrdinalIgnoreCase)); + + if (projectPath?.FullName is null) + throw new InvalidOperationException($"Project '{projectName}' not found in current directory."); + + return fileSystem + .FileSystem + .Path + .GetRelativePath(fileSystem.AtomRootDirectory, projectPath.FullName) + .Replace("\\", "/"); + } +} diff --git a/DecSm.Atom.Module.GithubWorkflows/Generation/GithubWorkflowType.cs b/src/Invex.Atom.Module.GithubWorkflows/GithubActions/GithubWorkflowType.cs similarity index 67% rename from DecSm.Atom.Module.GithubWorkflows/Generation/GithubWorkflowType.cs rename to src/Invex.Atom.Module.GithubWorkflows/GithubActions/GithubWorkflowType.cs index c932ed5f..7bb04e7b 100644 --- a/DecSm.Atom.Module.GithubWorkflows/Generation/GithubWorkflowType.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/GithubActions/GithubWorkflowType.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Generation; +namespace Invex.Atom.Module.GithubWorkflows.GithubActions; [PublicAPI] public sealed record GithubWorkflowType : IWorkflowType diff --git a/DecSm.Atom.Module.GithubWorkflows/GithubSummaryOutcomeReportWriter.cs b/src/Invex.Atom.Module.GithubWorkflows/GithubSummaryOutcomeReportWriter.cs similarity index 94% rename from DecSm.Atom.Module.GithubWorkflows/GithubSummaryOutcomeReportWriter.cs rename to src/Invex.Atom.Module.GithubWorkflows/GithubSummaryOutcomeReportWriter.cs index 3fe8d70a..33bb4f45 100644 --- a/DecSm.Atom.Module.GithubWorkflows/GithubSummaryOutcomeReportWriter.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/GithubSummaryOutcomeReportWriter.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Module.GithubWorkflows; +namespace Invex.Atom.Module.GithubWorkflows; /// /// Implements to write build outcome reports to GitHub Actions step summary. @@ -9,7 +9,7 @@ /// It also masks any secrets present in the report text. /// internal sealed class GithubSummaryOutcomeReportWriter( - IAtomFileSystem fileSystem, + IRootedFileSystem fileSystem, ReportService reportService, IParamService paramService ) : IOutcomeReportWriter diff --git a/DecSm.Atom.Module.GithubWorkflows/GithubVariableProvider.cs b/src/Invex.Atom.Module.GithubWorkflows/GithubVariableProvider.cs similarity index 89% rename from DecSm.Atom.Module.GithubWorkflows/GithubVariableProvider.cs rename to src/Invex.Atom.Module.GithubWorkflows/GithubVariableProvider.cs index 36cd4cc5..93d36ef6 100644 --- a/DecSm.Atom.Module.GithubWorkflows/GithubVariableProvider.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/GithubVariableProvider.cs @@ -1,14 +1,16 @@ -namespace DecSm.Atom.Module.GithubWorkflows; +using Environment = System.Environment; + +namespace Invex.Atom.Module.GithubWorkflows; /// -/// Provides an implementation of for GitHub Actions. +/// Provides an implementation of for GitHub Actions. /// /// /// This provider enables writing output variables that can be consumed by subsequent steps or jobs /// within a GitHub Actions workflow. It also indicates whether a variable can be read from a previous job. /// -internal sealed class GithubVariableProvider(IAtomFileSystem fileSystem, ILogger logger) - : IWorkflowVariableProvider +internal sealed class GithubVariableProvider(IRootedFileSystem fileSystem, ILogger logger) + : IVariableProvider { /// /// Writes a variable to the GitHub Actions output, making it available to subsequent steps or jobs. diff --git a/src/Invex.Atom.Module.GithubWorkflows/GithubWorkflowContextProvider.cs b/src/Invex.Atom.Module.GithubWorkflows/GithubWorkflowContextProvider.cs new file mode 100644 index 00000000..5e08ee97 --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/GithubWorkflowContextProvider.cs @@ -0,0 +1,14 @@ +namespace Invex.Atom.Module.GithubWorkflows; + +internal sealed class GithubWorkflowContextProvider : IWorkflowContextProvider +{ + public IWorkflowType? WorkflowType => + Github.IsGithubActions + ? WorkflowTypes.Github.Action + : null; + + public string? WorkflowName => + Github.IsGithubActions + ? Github.Variables.Workflow + : null; +} diff --git a/DecSm.Atom.Module.GithubWorkflows/IGithubHelper.cs b/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubHelper.cs similarity index 90% rename from DecSm.Atom.Module.GithubWorkflows/IGithubHelper.cs rename to src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubHelper.cs index d4c67dbe..eeb3b3a7 100644 --- a/DecSm.Atom.Module.GithubWorkflows/IGithubHelper.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubHelper.cs @@ -1,12 +1,13 @@ -namespace DecSm.Atom.Module.GithubWorkflows; +namespace Invex.Atom.Module.GithubWorkflows.Helpers; /// -/// Provides common GitHub-related parameters and functionality for DecSm.Atom builds. +/// Provides common GitHub-related parameters and functionality for Invex.Atom builds. /// /// /// Implementing this interface makes the GitHub token available as a secret parameter, /// which is often required for interacting with the GitHub API within workflows. /// +[PublicAPI] public interface IGithubHelper : IBuildAccessor { /// diff --git a/DecSm.Atom.Module.GithubWorkflows/IGithubReleaseHelper.cs b/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseHelper.cs similarity index 87% rename from DecSm.Atom.Module.GithubWorkflows/IGithubReleaseHelper.cs rename to src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseHelper.cs index 76ced3d3..f99a0074 100644 --- a/DecSm.Atom.Module.GithubWorkflows/IGithubReleaseHelper.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubReleaseHelper.cs @@ -1,12 +1,13 @@ -namespace DecSm.Atom.Module.GithubWorkflows; +namespace Invex.Atom.Module.GithubWorkflows.Helpers; /// -/// Provides helper methods for interacting with GitHub Releases within DecSm.Atom builds. +/// 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. /// +[PublicAPI] public interface IGithubReleaseHelper : IGithubHelper { /// @@ -32,7 +33,7 @@ async Task UploadArtifactToRelease( string releaseTag, bool dryRunWhenNotRunningInGithubActions = true) { - var artifactPath = FileSystem.AtomArtifactsDirectory / artifactName; + var artifactPath = RootedFileSystem.AtomArtifactsDirectory / artifactName; await UploadAssetToRelease(releaseTag, artifactPath, dryRunWhenNotRunningInGithubActions); } @@ -69,7 +70,7 @@ async Task UploadAssetToRelease( if (assetPath.DirectoryExists) Logger.LogInformation("Artifact path {AssetPath} is a directory containing {FileCount} files.", assetPath, - FileSystem.Directory.GetFiles(assetPath, "*", SearchOption.AllDirectories) + RootedFileSystem.Directory.GetFiles(assetPath, "*", SearchOption.AllDirectories) .Length); else Logger.LogInformation("Artifact path {AssetPath} is a file.", assetPath); @@ -77,7 +78,7 @@ async Task UploadAssetToRelease( return; } - var client = new GitHubClient(new("DecSm.Atom"), new InMemoryCredentialStore(new(GithubToken))); + var client = new GitHubClient(new("Invex.Atom"), new InMemoryCredentialStore(new(GithubToken))); var releases = await client.Repository.Release.GetAll(long.Parse(Github.Variables.RepositoryId)); @@ -85,7 +86,7 @@ async Task UploadAssetToRelease( if (assetPath.DirectoryExists) { - var zipPath = FileSystem.CreateRootedPath($"{assetPath}.zip"); + var zipPath = RootedFileSystem.CreateRootedPath($"{assetPath}.zip"); #if NET10_0_OR_GREATER await ZipFile.CreateFromDirectoryAsync(assetPath, zipPath); @@ -96,12 +97,12 @@ async Task UploadAssetToRelease( assetPath = zipPath; } - var assetFile = FileSystem.FileInfo.New(assetPath); + var assetFile = RootedFileSystem.FileInfo.New(assetPath); if (!assetFile.FullName.EndsWith(".zip")) { // zip the file - var zipPath = FileSystem.CreateRootedPath($"{assetPath}.zip"); + var zipPath = RootedFileSystem.CreateRootedPath($"{assetPath}.zip"); #if NET10_0_OR_GREATER await ZipFile.CreateFromDirectoryAsync(assetPath.Parent!, zipPath); @@ -112,7 +113,7 @@ async Task UploadAssetToRelease( assetPath = zipPath; } - assetFile = FileSystem.FileInfo.New(assetPath); + assetFile = RootedFileSystem.FileInfo.New(assetPath); await using var stream = assetFile.OpenRead(); diff --git a/DecSm.Atom.Module.GithubWorkflows/IGithubWorkflows.cs b/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubWorkflows.cs similarity index 75% rename from DecSm.Atom.Module.GithubWorkflows/IGithubWorkflows.cs rename to src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubWorkflows.cs index a104eec1..2a9593bc 100644 --- a/DecSm.Atom.Module.GithubWorkflows/IGithubWorkflows.cs +++ b/src/Invex.Atom.Module.GithubWorkflows/Helpers/IGithubWorkflows.cs @@ -1,13 +1,14 @@ -namespace DecSm.Atom.Module.GithubWorkflows; +namespace Invex.Atom.Module.GithubWorkflows.Helpers; /// -/// Provides integration with GitHub Actions workflows for DecSm.Atom builds. +/// Provides integration with GitHub Actions workflows for Invex.Atom builds. /// /// /// Implementing this interface configures the necessary services for generating /// GitHub Actions workflow files, providing GitHub-specific workflow variables, /// and adapting build paths and reporting when running within GitHub Actions. /// +[PublicAPI] [ConfigureHostBuilder] public partial interface IGithubWorkflows : IJobRunsOn { @@ -16,26 +17,30 @@ public partial interface IGithubWorkflows : IJobRunsOn /// /// The host application builder. /// - /// This method registers and + /// This method registers and /// for generating workflow files, and for GitHub-specific /// workflow variables. When running inside GitHub Actions, it also configures verbose logging, /// sets up for reporting, and adjusts /// artifact and publish paths to GitHub Actions conventions. /// - protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) + protected static partial void ConfigureBuilderFromIGithubWorkflows(IHostApplicationBuilder builder) { builder.Services.TryAddEnumerable(new ServiceDescriptor(typeof(IWorkflowWriter), - typeof(GithubWorkflowWriter), + typeof(GithubWorkflowFileWriter), ServiceLifetime.Singleton)); builder.Services.TryAddEnumerable(new ServiceDescriptor(typeof(IWorkflowWriter), - typeof(DependabotWorkflowWriter), + typeof(DependabotConfigFileWriter), ServiceLifetime.Singleton)); - builder.Services.TryAddEnumerable(new ServiceDescriptor(typeof(IWorkflowVariableProvider), + builder.Services.TryAddEnumerable(new ServiceDescriptor(typeof(IVariableProvider), typeof(GithubVariableProvider), ServiceLifetime.Singleton)); + builder.Services.TryAddEnumerable(new ServiceDescriptor(typeof(IWorkflowContextProvider), + typeof(GithubWorkflowContextProvider), + ServiceLifetime.Singleton)); + if (!Github.IsGithubActions) return; diff --git a/DecSm.Atom.Module.GithubWorkflows/DecSm.Atom.Module.GithubWorkflows.csproj b/src/Invex.Atom.Module.GithubWorkflows/Invex.Atom.Module.GithubWorkflows.csproj similarity index 68% rename from DecSm.Atom.Module.GithubWorkflows/DecSm.Atom.Module.GithubWorkflows.csproj rename to src/Invex.Atom.Module.GithubWorkflows/Invex.Atom.Module.GithubWorkflows.csproj index 0683e6f1..7b17d19c 100644 --- a/DecSm.Atom.Module.GithubWorkflows/DecSm.Atom.Module.GithubWorkflows.csproj +++ b/src/Invex.Atom.Module.GithubWorkflows/Invex.Atom.Module.GithubWorkflows.csproj @@ -5,10 +5,12 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -24,12 +26,12 @@ - - + + - + \ No newline at end of file diff --git a/src/Invex.Atom.Module.GithubWorkflows/Invex.Atom.Module.GithubWorkflows.props b/src/Invex.Atom.Module.GithubWorkflows/Invex.Atom.Module.GithubWorkflows.props new file mode 100644 index 00000000..fd678d0d --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Invex.Atom.Module.GithubWorkflows.props @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Invex.Atom.Module.GithubWorkflows/Options/GithubRunsOn.cs b/src/Invex.Atom.Module.GithubWorkflows/Options/GithubRunsOn.cs new file mode 100644 index 00000000..c94df9ca --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Options/GithubRunsOn.cs @@ -0,0 +1,9 @@ +namespace Invex.Atom.Module.GithubWorkflows.Options; + +[PublicAPI] +public sealed record GithubRunsOn : IBuildOption +{ + public TextExpressionCollection Labels { get; init; } = []; + + public TextExpression? Group { get; init; } +} diff --git a/src/Invex.Atom.Module.GithubWorkflows/Options/GithubTokenPermissionsOption.cs b/src/Invex.Atom.Module.GithubWorkflows/Options/GithubTokenPermissionsOption.cs new file mode 100644 index 00000000..79e00bfa --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Options/GithubTokenPermissionsOption.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Module.GithubWorkflows.Options; + +[PublicAPI] +public sealed record GithubTokenPermissionsOption(Permissions Permissions) : IBuildOption; diff --git a/src/Invex.Atom.Module.GithubWorkflows/Options/GithubTrigger.cs b/src/Invex.Atom.Module.GithubWorkflows/Options/GithubTrigger.cs new file mode 100644 index 00000000..2d80eeb5 --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Options/GithubTrigger.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Module.GithubWorkflows.Options; + +[PublicAPI] +public sealed record GithubTrigger(On On) : IWorkflowTrigger; diff --git a/src/Invex.Atom.Module.GithubWorkflows/Steps/GithubCheckoutStep.cs b/src/Invex.Atom.Module.GithubWorkflows/Steps/GithubCheckoutStep.cs new file mode 100644 index 00000000..1f85aa46 --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Steps/GithubCheckoutStep.cs @@ -0,0 +1,239 @@ +namespace Invex.Atom.Module.GithubWorkflows.Steps; + +[PublicAPI] +public sealed record GithubCheckoutStep : CheckoutStep, IGithubAdditionalStepOption +{ + /// + /// Repository name with owner. For example, actions/checkout. + /// + /// Default: ${{ github.repository }} + public TextExpression? Repository { get; init; } + + /// + /// The branch, tag or SHA to checkout. When checking out the repository that + /// triggered a workflow, this defaults to the reference or SHA for that event. + /// Otherwise, uses the default branch. + /// + public TextExpression? Branch { get; init; } + + /// + /// Personal access token (PAT) used to fetch the repository. The PAT is configured + /// with the local git config, which enables your scripts to run authenticated git + /// commands. The post-job step removes the PAT. + /// + /// + /// + /// We recommend using a service account with the least permissions necessary. Also + /// when generating a new PAT, select the least scopes necessary. + /// + /// + /// + /// Learn + /// more about creating and using encrypted secrets + /// + /// + /// Default: ${{ github.token }} + /// + public TextExpression? Token { get; init; } + + /// + /// SSH key used to fetch the repository. The SSH key is configured with the local + /// git config, which enables your scripts to run authenticated git commands. The + /// post-job step removes the SSH key. + /// + /// + /// + /// We recommend using a service account with the least permissions necessary. + /// + /// + /// + /// Learn + /// more about creating and using encrypted secrets + /// + /// + /// + public TextExpression? SshKey { get; init; } + + /// + /// Known hosts in addition to the user and global host key database. The public SSH + /// keys for a host may be obtained using the utility ssh-keyscan. For example, + /// ssh-keyscan github.com. The public key for github.com is always implicitly + /// added. + /// + public TextExpression? SshKnownHosts { get; init; } + + /// + /// Whether to perform strict host key checking. When true, adds the options + /// StrictHostKeyChecking=yes and CheckHostIP=no to the SSH command line. Use + /// the input ssh-known-hosts to configure additional hosts. + /// + /// Default: true + public TextExpression? SshStrict { get; init; } + + /// + /// The user to use when connecting to the remote SSH host. By default 'git' is used. + /// + /// Default: git + public TextExpression? SshUser { get; init; } + + /// + /// Whether to configure the token or SSH key with the local git config. + /// + /// Default: true + public TextExpression? PersistCredentials { get; init; } + + /// + /// Relative path under $GITHUB_WORKSPACE to place the repository. + /// + public TextExpression? Path { get; init; } + + /// + /// Whether to execute git clean -ffdx && git reset --hard HEAD before fetching. + /// + /// Default: true + public TextExpression? Clean { get; init; } + + /// + /// Partially clone against a given filter. Overrides sparse-checkout if set. + /// + /// Default: null + public TextExpression? Filter { get; init; } + + /// + /// Do a sparse checkout on given patterns. Each pattern should be separated with new lines. + /// + /// Default: null + public TextExpression? SparseCheckout { get; init; } + + /// + /// Specifies whether to use cone-mode when doing a sparse checkout. + /// + /// Default: true + public TextExpression? SparseCheckoutConeMode { get; init; } + + /// + /// Number of commits to fetch. 0 indicates all history for all branches and tags. + /// + /// Default: 1 + public TextExpression? FetchDepth { get; init; } + + /// + /// Whether to fetch tags, even if fetch-depth > 0. + /// + /// Default: false + public TextExpression? FetchTags { get; init; } + + /// + /// Whether to show progress status output when fetching. + /// + /// Default: true + public TextExpression? ShowProgress { get; init; } + + /// + /// Whether to download Git-LFS files. + /// + /// Default: false + public TextExpression? Lfs { get; init; } + + /// + /// Whether to checkout submodules: true to checkout submodules or recursive to + /// recursively checkout submodules. + /// + /// + /// + /// When the ssh-key input is not provided, SSH URLs beginning with + /// git@github.com: are converted to HTTPS. + /// + /// Default: false + /// + public TextExpression? Submodules { get; init; } + + /// + /// Add repository path as safe.directory for Git global config by running + /// git config --global --add safe.directory <path>. + /// + /// Default: true + public TextExpression? SetSafeDirectory { get; init; } + + /// + /// The base URL for the GitHub instance that you are trying to clone from, will use + /// environment defaults to fetch from the same instance that the workflow is + /// running from unless specified. Example URLs are https://github.com or + /// https://my-ghes-server.example.com. + /// + public TextExpression? GithubServerUrl { get; init; } + + public Step Build() + { + var with = new Dictionary(); + + if (Repository is not null) + with["repository"] = [Repository]; + + if (Branch is not null) + with["ref"] = [Branch]; + + if (Token is not null) + with["token"] = [Token]; + + if (SshKey is not null) + with["ssh-key"] = [SshKey]; + + if (SshKnownHosts is not null) + with["ssh-known-hosts"] = [SshKnownHosts]; + + if (SshStrict is not null) + with["ssh-strict"] = [SshStrict]; + + if (SshUser is not null) + with["ssh-user"] = [SshUser]; + + if (PersistCredentials is not null) + with["persist-credentials"] = [PersistCredentials]; + + if (Path is not null) + with["path"] = [Path]; + + if (Clean is not null) + with["clean"] = [Clean]; + + if (Filter is not null) + with["filter"] = [Filter]; + + if (SparseCheckout is not null) + with["sparse-checkout"] = [SparseCheckout]; + + if (SparseCheckoutConeMode is not null) + with["sparse-checkout-cone-mode"] = [SparseCheckoutConeMode]; + + if (FetchDepth is not null) + with["fetch-depth"] = [FetchDepth]; + + if (FetchTags is not null) + with["fetch-tags"] = [FetchTags]; + + if (ShowProgress is not null) + with["show-progress"] = [ShowProgress]; + + if (Lfs is not null) + with["lfs"] = [Lfs]; + + if (Submodules is not null) + with["submodules"] = [Submodules]; + + if (SetSafeDirectory is not null) + with["set-safe-directory"] = [SetSafeDirectory]; + + if (GithubServerUrl is not null) + with["github-server-url"] = [GithubServerUrl]; + + return new Step.UsesStep + { + Name = "Checkout", + With = with, + Uses = "actions/checkout@v6", + }; + } +} diff --git a/src/Invex.Atom.Module.GithubWorkflows/Steps/GithubCustomAdditionalStep.cs b/src/Invex.Atom.Module.GithubWorkflows/Steps/GithubCustomAdditionalStep.cs new file mode 100644 index 00000000..0986ab5b --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Steps/GithubCustomAdditionalStep.cs @@ -0,0 +1,14 @@ +namespace Invex.Atom.Module.GithubWorkflows.Steps; + +[PublicAPI] +public sealed record GithubCustomAdditionalStep : IGithubAdditionalStepOption +{ + public required Step Step { get; init; } + + public bool Enabled { get; init; } = true; + + public required int Order { get; init; } + + public Step Build() => + Step; +} diff --git a/src/Invex.Atom.Module.GithubWorkflows/Steps/IGithubAdditionalStepOption.cs b/src/Invex.Atom.Module.GithubWorkflows/Steps/IGithubAdditionalStepOption.cs new file mode 100644 index 00000000..6ab052aa --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/Steps/IGithubAdditionalStepOption.cs @@ -0,0 +1,7 @@ +namespace Invex.Atom.Module.GithubWorkflows.Steps; + +[PublicAPI] +public interface IGithubAdditionalStepOption : IAdditionalStepOption +{ + Step Build(); +} diff --git a/src/Invex.Atom.Module.GithubWorkflows/_usings.cs b/src/Invex.Atom.Module.GithubWorkflows/_usings.cs new file mode 100644 index 00000000..32a2f4da --- /dev/null +++ b/src/Invex.Atom.Module.GithubWorkflows/_usings.cs @@ -0,0 +1,43 @@ +global using System.Diagnostics.CodeAnalysis; +global using System.IO.Compression; +global using System.Reflection; +global using System.Text; +global using Invex.Atom.Build.Artifacts; +global using Invex.Atom.Build; +global using Invex.Atom.Build.BuildOptions; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.Model; +global using Invex.Atom.Build.Logging; +global using Invex.Atom.Build.Params; +global using Invex.Atom.Build.Reports; +global using Invex.Atom.Build.Variables; +global using Invex.Atom.Workflows.Definition; +global using Invex.Atom.Workflows.Definition.Triggers; +global using Invex.Atom.Workflows.Model; +global using Invex.Atom.Workflows.Options; +global using Invex.Atom.Workflows.Options.Injections; +global using Invex.Atom.Workflows.Writer; +global using JetBrains.Annotations; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Octokit; +global using Octokit.Internal; +global using System.Runtime.CompilerServices; +global using System.Runtime.InteropServices; +global using Invex.Atom.Build.FileSystem; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Module.GithubWorkflows.DependabotConfig; +global using Invex.Atom.Module.GithubWorkflows.Extensions; +global using Invex.Atom.Module.GithubWorkflows.GithubActions; +global using Invex.Atom.Module.GithubWorkflows.Helpers; +global using Invex.Atom.Module.GithubWorkflows.Options; +global using Invex.Atom.Module.GithubWorkflows.Steps; +global using Invex.Atom.Workflows; +global using Invex.Atom.Workflows.Dotnet.Nuget; +global using Invex.Atom.Workflows.WorkflowContext; +global using Invex.StructuredText.GithubActions; +global using Invex.StructuredText.GithubActions.GithubActionModel; + +[assembly: InternalsVisibleTo("Invex.Atom.Module.GithubWorkflows.Tests")] diff --git a/src/Invex.Atom.Tool/BuildCache.cs b/src/Invex.Atom.Tool/BuildCache.cs new file mode 100644 index 00000000..7bb43205 --- /dev/null +++ b/src/Invex.Atom.Tool/BuildCache.cs @@ -0,0 +1,135 @@ +namespace Invex.Atom.Tool; + +/// +/// Content-hash based cache that lets decide whether a +/// dotnet build can be skipped (by passing --no-build to dotnet run). +/// +/// +/// The cache hashes the project's restore inputs plus every .cs source file under the project +/// directory (excluding bin/obj), and is stored in obj/.atom-build.hash. A build is +/// only skippable when a previous build output (bin/<project>.dll) exists and the cached hash +/// matches. +/// +/// Note: source changes in projects referenced via <ProjectReference> are not tracked. +/// Use --no-restore-cache (or the ATOM_NO_RESTORE_CACHE environment variable) to force a +/// full restore and build. +/// +/// +internal static class BuildCache +{ + private const string CacheFileName = ".atom-build.hash"; + + private static readonly string[] ExcludedFolderNames = ["bin", "obj"]; + + /// + /// Represents the outcome of evaluating the build cache for a given target. + /// + /// Whether the build can be skipped because nothing relevant changed. + /// The freshly computed hash of the build-affecting inputs. + /// The path of the cache file used to persist the hash. + internal sealed record BuildDecision(bool CanSkipBuild, string ComputedHash, string CacheFilePath); + + /// + /// Computes the build input hash and compares it against the cached value to determine whether a + /// build can be skipped. + /// + internal static BuildDecision Evaluate(IFileSystem fileSystem, IFileInfo target) + { + var projectDir = target.Directory?.FullName ?? fileSystem.Path.GetDirectoryName(target.FullName)!; + var objDir = fileSystem.Path.Combine(projectDir, "obj"); + var cacheFilePath = fileSystem.Path.Combine(objDir, CacheFileName); + + var inputs = RestoreCache.CollectRestoreInputs(fileSystem, target, projectDir); + var sources = CollectSourceFiles(fileSystem, projectDir); + + var computedHash = HashCache.ComputeHash(fileSystem, projectDir, inputs.Concat(sources)); + + // A build can only be skipped if a previous build actually produced an assembly and the cached hash + // matches the current inputs. + var canSkip = HasBuildOutput(fileSystem, projectDir, target) && + fileSystem.File.Exists(cacheFilePath) && + string.Equals(HashCache.TryRead(fileSystem, cacheFilePath), + computedHash, + StringComparison.OrdinalIgnoreCase); + + return new(canSkip, computedHash, cacheFilePath); + } + + /// + /// Persists the computed hash so a subsequent run can skip the build. Best-effort: failures are ignored. + /// + internal static void Save(IFileSystem fileSystem, BuildDecision decision) => + HashCache.Write(fileSystem, decision.CacheFilePath, decision.ComputedHash); + + private static bool HasBuildOutput(IFileSystem fileSystem, string projectDir, IFileInfo target) + { + var binDir = fileSystem.Path.Combine(projectDir, "bin"); + + if (!fileSystem.Directory.Exists(binDir)) + return false; + + var assemblyFileName = $"{fileSystem.Path.GetFileNameWithoutExtension(target.Name)}.dll"; + + try + { + return fileSystem + .Directory + .EnumerateFiles(binDir, assemblyFileName, SearchOption.AllDirectories) + .Any(); + } + catch (IOException) + { + return false; + } + catch (UnauthorizedAccessException) + { + return false; + } + } + + private static List CollectSourceFiles(IFileSystem fileSystem, string projectDir) + { + var result = new List(); + var queue = new Queue(); + queue.Enqueue(projectDir); + + while (queue.Count > 0) + { + var dir = queue.Dequeue(); + + try + { + result.AddRange(fileSystem.Directory.EnumerateFiles(dir, "*.cs")); + } + catch (IOException) + { + continue; + } + catch (UnauthorizedAccessException) + { + continue; + } + + try + { + foreach (var sub in fileSystem.Directory.EnumerateDirectories(dir)) + { + var name = fileSystem.Path.GetFileName(sub); + + if (!ExcludedFolderNames.Contains(name, StringComparer.OrdinalIgnoreCase)) + queue.Enqueue(sub); + } + } + catch (IOException) + { + // Ignore directories we cannot enumerate. + } + catch (UnauthorizedAccessException) + { + // Ignore directories we cannot access. + } + } + + return result; + } +} diff --git a/DecSm.Atom.Tool/CommandModel.cs b/src/Invex.Atom.Tool/CommandModel.cs similarity index 81% rename from DecSm.Atom.Tool/CommandModel.cs rename to src/Invex.Atom.Tool/CommandModel.cs index 8c1e4e57..5729833c 100644 --- a/DecSm.Atom.Tool/CommandModel.cs +++ b/src/Invex.Atom.Tool/CommandModel.cs @@ -1,36 +1,39 @@ #pragma warning disable CA1822, RCS1163 -namespace DecSm.Atom.Tool; +namespace Invex.Atom.Tool; /// -/// Defines the command-line interface (CLI) commands available in the DecSm.Atom.Tool. +/// Defines the command-line interface (CLI) commands available in the Invex.Atom.Tool. /// /// /// This class uses ConsoleAppFramework to expose various functionalities, -/// allowing users to interact with DecSm.Atom projects and NuGet configurations +/// allowing users to interact with Invex.Atom projects and NuGet configurations /// directly from the command line. /// [PublicAPI] internal sealed class CommandModel { /// - /// Runs the specified DecSm.Atom project with the given arguments. + /// Runs the specified Invex.Atom project with the given arguments. /// This is the default command executed when no specific subcommand is provided. /// /// The console app context, providing access to raw arguments. /// - /// Optional. Arguments to pass directly to the DecSm.Atom build project. + /// Optional. Arguments to pass directly to the Invex.Atom build project. /// These arguments are typically used to specify targets or parameters for the build. /// /// - /// Optional. The name of the DecSm.Atom project or file-based app to run. Defaults to "_atom". + /// Optional. The name of the Invex.Atom project or file-based app to run. Defaults to "_atom". /// This allows specifying which build definition to execute within a multi-project solution. /// /// - /// Optional. The path to a C# file to run as a file-based DecSm.Atom application. + /// Optional. The path to a C# file to run as a file-based Invex.Atom application. + /// + /// + /// Optional. When set, bypasses the restore cache and always performs a dotnet restore. /// /// A cancellation token to observe while waiting for the command to complete. - /// The exit code of the executed DecSm.Atom build command. + /// The exit code of the executed Invex.Atom build command. [Command("")] #pragma warning disable CA1822 public Task Root( @@ -38,6 +41,7 @@ public Task Root( [Argument] string[]? runArgs = null, [HideDefaultValue] string? project = null, [HideDefaultValue] string? file = null, + bool noRestoreCache = false, CancellationToken cancellationToken = default) => RunCommand.Handle(context .Arguments @@ -48,6 +52,7 @@ public Task Root( : file is { Length: > 0 } ? file : string.Empty, + noRestoreCache, cancellationToken); /// diff --git a/DecSm.Atom.Tool/Commands/NugetAddCommand.cs b/src/Invex.Atom.Tool/Commands/NugetAddCommand.cs similarity index 99% rename from DecSm.Atom.Tool/Commands/NugetAddCommand.cs rename to src/Invex.Atom.Tool/Commands/NugetAddCommand.cs index 57f18a29..29b6ea33 100644 --- a/DecSm.Atom.Tool/Commands/NugetAddCommand.cs +++ b/src/Invex.Atom.Tool/Commands/NugetAddCommand.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Tool.Commands; +namespace Invex.Atom.Tool.Commands; /// /// Handles the command for adding a NuGet package source to the user's NuGet configuration. diff --git a/DecSm.Atom.Tool/Commands/RunCommand.cs b/src/Invex.Atom.Tool/Commands/RunCommand.cs similarity index 50% rename from DecSm.Atom.Tool/Commands/RunCommand.cs rename to src/Invex.Atom.Tool/Commands/RunCommand.cs index 3b13b468..9906d27d 100644 --- a/DecSm.Atom.Tool/Commands/RunCommand.cs +++ b/src/Invex.Atom.Tool/Commands/RunCommand.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tool.Commands; +namespace Invex.Atom.Tool.Commands; /// -/// Handles the execution of a DecSm.Atom build project. +/// Handles the execution of a Invex.Atom build project. /// /// /// This command locates the specified Atom project in the current directory or its parent directories, @@ -16,14 +16,50 @@ internal static class RunCommand public static bool MockDotnetCli { get; set; } /// - /// Executes the specified DecSm.Atom project. + /// Indicates whether the most recent call passed --no-restore to + /// dotnet run (i.e. the restore was skipped because of the restore cache). Exposed for testing. /// - /// Arguments to pass directly to the DecSm.Atom project. - /// The name of the DecSm.Atom project or file-based app to run (e.g., "_atom"). + [UsedImplicitly(Reason = "Used in tests")] + public static bool LastUsedNoRestore { get; private set; } + + /// + /// Indicates whether the most recent call passed --no-build to + /// dotnet run (i.e. the build was skipped because of the build cache). Exposed for testing. + /// + [UsedImplicitly(Reason = "Used in tests")] + public static bool LastUsedNoBuild { get; private set; } + + /// + /// Executes the specified Invex.Atom project. + /// + /// Arguments to pass directly to the Invex.Atom project. + /// The name of the Invex.Atom project or file-based app to run (e.g., "_atom"). + /// A cancellation token. + /// The exit code of the executed `dotnet run` command. + public static Task Handle(string[] runArgs, string subject, CancellationToken cancellationToken) => + Handle(runArgs, subject, false, cancellationToken); + + /// + /// Executes the specified Invex.Atom project. + /// + /// Arguments to pass directly to the Invex.Atom project. + /// The name of the Invex.Atom project or file-based app to run (e.g., "_atom"). + /// + /// When true, the restore cache is bypassed and a normal restore is always performed. + /// /// A cancellation token. /// The exit code of the executed `dotnet run` command. - public static async Task Handle(string[] runArgs, string subject, CancellationToken cancellationToken) + public static async Task Handle( + string[] runArgs, + string subject, + bool noRestoreCache, + CancellationToken cancellationToken) { + LastUsedNoRestore = false; + LastUsedNoBuild = false; + + var forceRestore = noRestoreCache || IsRestoreCacheDisabledByEnv(); + subject = subject .Replace("\n", string.Empty) .Replace("\r", string.Empty) @@ -41,7 +77,8 @@ var s when s.EndsWith(".csproj") => SubjectInputType.Project, { case SubjectInputType.Project: { - var knownProjectResult = await FindAndExecuteKnownProject(subject, runArgs, cancellationToken); + var knownProjectResult = + await FindAndExecuteKnownProject(subject, runArgs, forceRestore, cancellationToken); if (knownProjectResult is not null) return knownProjectResult.Value; @@ -53,7 +90,7 @@ var s when s.EndsWith(".csproj") => SubjectInputType.Project, case SubjectInputType.File: { - var knownFileResult = await FindAndExecuteKnownFile(subject, runArgs, cancellationToken); + var knownFileResult = await FindAndExecuteKnownFile(subject, runArgs, forceRestore, cancellationToken); if (knownFileResult is not null) return knownFileResult.Value; @@ -65,7 +102,7 @@ var s when s.EndsWith(".csproj") => SubjectInputType.Project, case SubjectInputType.Either: { - var eitherResult = await FindAndExecuteKnownEither(subject, runArgs, cancellationToken); + var eitherResult = await FindAndExecuteKnownEither(subject, runArgs, forceRestore, cancellationToken); if (eitherResult is not null) return eitherResult.Value; @@ -77,7 +114,7 @@ var s when s.EndsWith(".csproj") => SubjectInputType.Project, case SubjectInputType.None: { - var noneResult = await FindAndExecuteUnknown(runArgs, cancellationToken); + var noneResult = await FindAndExecuteUnknown(runArgs, forceRestore, cancellationToken); if (noneResult is not null) return noneResult.Value; @@ -96,6 +133,7 @@ await Console.Error.WriteLineAsync( private static async Task FindAndExecuteKnownProject( string name, string[] runArgs, + bool forceRestore, CancellationToken cancellationToken) { if (!name.EndsWith(".csproj")) @@ -104,7 +142,7 @@ await Console.Error.WriteLineAsync( var foundPath = FileFinder.FindFile(FileSystem, FileSystem.Directory.GetCurrentDirectory(), [name], true); if (foundPath is not null) - return await Execute(foundPath, runArgs, false, cancellationToken); + return await Execute(foundPath, runArgs, false, forceRestore, cancellationToken); return null; } @@ -112,6 +150,7 @@ await Console.Error.WriteLineAsync( private static async Task FindAndExecuteKnownFile( string name, string[] runArgs, + bool forceRestore, CancellationToken cancellationToken) { if (!name.EndsWith(".cs")) @@ -120,7 +159,7 @@ await Console.Error.WriteLineAsync( var foundPath = FileFinder.FindFile(FileSystem, FileSystem.Directory.GetCurrentDirectory(), [name], false); if (foundPath is not null) - return await Execute(foundPath, runArgs, true, cancellationToken); + return await Execute(foundPath, runArgs, true, forceRestore, cancellationToken); return null; } @@ -128,6 +167,7 @@ await Console.Error.WriteLineAsync( private static async Task FindAndExecuteKnownEither( string name, string[] runArgs, + bool forceRestore, CancellationToken cancellationToken) { var currentDirectory = FileSystem.Directory.GetCurrentDirectory(); @@ -135,17 +175,20 @@ await Console.Error.WriteLineAsync( var projectPath = FileFinder.FindFile(FileSystem, currentDirectory, [name, $"{name}.csproj"], true); if (projectPath is not null) - return await Execute(projectPath, runArgs, false, cancellationToken); + return await Execute(projectPath, runArgs, false, forceRestore, cancellationToken); var csPath = FileFinder.FindFile(FileSystem, currentDirectory, [name, $"{name}.cs"], false); if (csPath is not null) - return await Execute(csPath, runArgs, true, cancellationToken); + return await Execute(csPath, runArgs, true, forceRestore, cancellationToken); return null; } - private static async Task FindAndExecuteUnknown(string[] runArgs, CancellationToken cancellationToken) + private static async Task FindAndExecuteUnknown( + string[] runArgs, + bool forceRestore, + CancellationToken cancellationToken) { // If on nix, we want to duplicate defaultNames for case-sensitivity string[] defaultNames = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) @@ -154,7 +197,7 @@ await Console.Error.WriteLineAsync( foreach (var name in defaultNames) { - var result = await FindAndExecuteKnownEither(name, runArgs, cancellationToken); + var result = await FindAndExecuteKnownEither(name, runArgs, forceRestore, cancellationToken); if (result is not null) return result; @@ -167,6 +210,7 @@ private static async Task Execute( IFileInfo path, string[] runArgs, bool isCsFile, + bool forceRestore, CancellationToken cancellationToken) { var sanitizedArgs = SanitizeArgs(runArgs); @@ -181,23 +225,89 @@ private static async Task Execute( args.Add(path.FullName); + // Decide whether the build and/or restore can be skipped based on hashes of their inputs. + // '--no-build' implies '--no-restore', so it takes precedence when applicable. + RestoreCache.RestoreDecision? restoreDecision = null; + BuildCache.BuildDecision? buildDecision = null; + var useNoBuild = false; + var useNoRestore = false; + + if (!forceRestore) + { + // The build cache only applies to compiled projects, not file-based (.cs) programs. + if (!isCsFile) + { + buildDecision = BuildCache.Evaluate(FileSystem, path); + useNoBuild = buildDecision.CanSkipBuild; + } + + if (!useNoBuild) + { + restoreDecision = RestoreCache.Evaluate(FileSystem, path); + useNoRestore = restoreDecision.CanSkipRestore; + } + } + + if (useNoBuild) + args.Add("--no-build"); + else if (useNoRestore) + args.Add("--no-restore"); + + LastUsedNoBuild = useNoBuild; + LastUsedNoRestore = useNoRestore; + args.Add("--"); args.AddRange(sanitizedArgs); if (MockDotnetCli) + { + // Simulate a successful run: persist the hashes so the next run can skip work. + PersistCaches(path, isCsFile, useNoBuild, useNoRestore, restoreDecision, buildDecision); + return 0; + } var atomProcess = Process.Start("dotnet", args); await atomProcess.WaitForExitAsync(cancellationToken); + // If work actually ran and the build succeeded, persist the hashes so the next run can skip it. + if (atomProcess.ExitCode is 0) + PersistCaches(path, isCsFile, useNoBuild, useNoRestore, restoreDecision, buildDecision); + return atomProcess.ExitCode; } + private static void PersistCaches( + IFileInfo path, + bool isCsFile, + bool useNoBuild, + bool useNoRestore, + RestoreCache.RestoreDecision? restoreDecision, + BuildCache.BuildDecision? buildDecision) + { + // A restore ran unless it (or the whole build) was skipped. + if (!useNoBuild && !useNoRestore) + RestoreCache.Save(FileSystem, restoreDecision ?? RestoreCache.Evaluate(FileSystem, path)); + + // A build ran unless it was skipped (build cache applies to compiled projects only). + if (!useNoBuild && !isCsFile) + BuildCache.Save(FileSystem, buildDecision ?? BuildCache.Evaluate(FileSystem, path)); + } + private static IEnumerable SanitizeArgs(IEnumerable runArgs) => runArgs.Select(arg => arg .Replace("\n", string.Empty) .Replace("\r", string.Empty)); + private static bool IsRestoreCacheDisabledByEnv() + { + var value = Environment.GetEnvironmentVariable("ATOM_NO_RESTORE_CACHE"); + + return value is { Length: > 0 } && + !string.Equals(value, "0", StringComparison.OrdinalIgnoreCase) && + !string.Equals(value, "false", StringComparison.OrdinalIgnoreCase); + } + private enum SubjectInputType { None, diff --git a/DecSm.Atom.Tool/FileFinder.cs b/src/Invex.Atom.Tool/FileFinder.cs similarity index 88% rename from DecSm.Atom.Tool/FileFinder.cs rename to src/Invex.Atom.Tool/FileFinder.cs index 27918ff8..5961475e 100644 --- a/DecSm.Atom.Tool/FileFinder.cs +++ b/src/Invex.Atom.Tool/FileFinder.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Tool; +namespace Invex.Atom.Tool; public static class FileFinder { @@ -57,19 +57,6 @@ public static class FileFinder "$RECYCLE.BIN", }; - /// - /// Markers that signal the "Root" of a project. - /// The upward search will stop here. - /// - private static readonly HashSet RootMarkers = new(StringComparer.OrdinalIgnoreCase) - { - ".git", - ".sln", - "package.json", - "go.mod", - "Cargo.toml", - }; - public static IFileInfo? FindFile( IFileSystem fileSystem, string startDirectory, @@ -93,9 +80,7 @@ public static class FileFinder return found; // Stop condition: Don't climb higher than a project root marker - if (RootMarkers.Any(marker => - fileSystem.File.Exists(fileSystem.Path.Combine(currentUp.FullName, marker)) || - fileSystem.Directory.Exists(fileSystem.Path.Combine(currentUp.FullName, marker)))) + if (IsRootDirectory(fileSystem, currentUp.FullName)) break; currentUp = currentUp.Parent; @@ -171,4 +156,26 @@ public static class FileFinder return null; } + + /// + /// Markers that signal the "Root" of a project. + /// The upward search will stop here. + /// + private static readonly HashSet RootMarkers = new(StringComparer.OrdinalIgnoreCase) + { + ".git", + ".slnx", + ".sln", + "package.json", + "go.mod", + "Cargo.toml", + }; + + /// + /// Determines whether the specified directory contains a project root marker + /// (e.g. a .git folder or a solution file). + /// + internal static bool IsRootDirectory(IFileSystem fileSystem, string directory) => + RootMarkers.Any(marker => fileSystem.File.Exists(fileSystem.Path.Combine(directory, marker)) || + fileSystem.Directory.Exists(fileSystem.Path.Combine(directory, marker))); } diff --git a/src/Invex.Atom.Tool/HashCache.cs b/src/Invex.Atom.Tool/HashCache.cs new file mode 100644 index 00000000..c3cea6ab --- /dev/null +++ b/src/Invex.Atom.Tool/HashCache.cs @@ -0,0 +1,81 @@ +namespace Invex.Atom.Tool; + +/// +/// Shared low-level helpers for the content-hash based restore/build caches used by +/// . +/// +internal static class HashCache +{ + /// + /// Computes a stable SHA256 hash over the given files. The hash incorporates each file's path + /// (relative to ) and its raw content, and is order-independent. + /// + internal static string ComputeHash(IFileSystem fileSystem, string projectDir, IEnumerable files) + { + var ordered = files + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(static x => x, StringComparer.Ordinal); + + using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); + + foreach (var file in ordered) + { + var relativePath = fileSystem.Path.GetRelativePath(projectDir, file); + + hash.AppendData(Encoding.UTF8.GetBytes(relativePath)); + + try + { + hash.AppendData(fileSystem.File.ReadAllBytes(file)); + } + catch (IOException) + { + // Treat an unreadable input as empty content rather than failing the run. + } + } + + return Convert.ToHexString(hash.GetHashAndReset()); + } + + /// + /// Reads the cached hash from the given file, or null if it cannot be read. + /// + internal static string? TryRead(IFileSystem fileSystem, string cacheFilePath) + { + try + { + return fileSystem + .File + .ReadAllText(cacheFilePath) + .Trim(); + } + catch (IOException) + { + return null; + } + } + + /// + /// Persists the computed hash. Best-effort: failures (e.g. concurrent runs, permissions) are ignored. + /// + internal static void Write(IFileSystem fileSystem, string cacheFilePath, string hash) + { + try + { + var dir = fileSystem.Path.GetDirectoryName(cacheFilePath); + + if (dir is { Length: > 0 }) + fileSystem.Directory.CreateDirectory(dir); + + fileSystem.File.WriteAllText(cacheFilePath, hash); + } + catch (IOException) + { + // Best-effort: a concurrent run or a locked file should never break the build. + } + catch (UnauthorizedAccessException) + { + // Best-effort: missing permissions should never break the build. + } + } +} diff --git a/DecSm.Atom.Tool/DecSm.Atom.Tool.csproj b/src/Invex.Atom.Tool/Invex.Atom.Tool.csproj similarity index 93% rename from DecSm.Atom.Tool/DecSm.Atom.Tool.csproj rename to src/Invex.Atom.Tool/Invex.Atom.Tool.csproj index 36bd4ebf..044da700 100644 --- a/DecSm.Atom.Tool/DecSm.Atom.Tool.csproj +++ b/src/Invex.Atom.Tool/Invex.Atom.Tool.csproj @@ -5,7 +5,7 @@ true true atom - DecSm.Atom.Tool + Invex.Atom.Tool @@ -62,4 +62,8 @@ + + + + \ No newline at end of file diff --git a/DecSm.Atom.Tool/Program.cs b/src/Invex.Atom.Tool/Program.cs similarity index 80% rename from DecSm.Atom.Tool/Program.cs rename to src/Invex.Atom.Tool/Program.cs index bac6eec8..bb8546ba 100644 --- a/DecSm.Atom.Tool/Program.cs +++ b/src/Invex.Atom.Tool/Program.cs @@ -1,4 +1,4 @@ -// The entry point for the DecSm.Atom.Tool console application. +// The entry point for the Invex.Atom.Tool console application. // This file sets up the command-line interface using ConsoleAppFramework, // registers the main command model, and applies a custom argument filter. diff --git a/src/Invex.Atom.Tool/RestoreCache.cs b/src/Invex.Atom.Tool/RestoreCache.cs new file mode 100644 index 00000000..3b8463e9 --- /dev/null +++ b/src/Invex.Atom.Tool/RestoreCache.cs @@ -0,0 +1,100 @@ +namespace Invex.Atom.Tool; + +/// +/// Provides a content-hash based cache that lets decide whether a +/// dotnet restore can be skipped (by passing --no-restore to dotnet run). +/// +/// +/// The cache is stored in the project's obj folder (so it is naturally invalidated by +/// dotnet clean) and keyed by a SHA256 hash of all restore-affecting inputs: the project/file +/// itself plus any Directory.Build.props, Directory.Build.targets, +/// Directory.Packages.props, nuget.config and global.json files found while +/// walking up to the project root. +/// +internal static class RestoreCache +{ + private const string CacheFileName = ".atom-restore.hash"; + + private const string RestoreMarkerFileName = "project.assets.json"; + + private static readonly string[] RestoreInputFileNames = + [ + "Directory.Build.props", + "Directory.Build.targets", + "Directory.Packages.props", + "nuget.config", + "global.json", + ]; + + /// + /// Represents the outcome of evaluating the restore cache for a given target. + /// + /// Whether the restore can be skipped because nothing relevant changed. + /// The freshly computed hash of the restore-affecting inputs. + /// The path of the cache file used to persist the hash. + internal sealed record RestoreDecision(bool CanSkipRestore, string ComputedHash, string CacheFilePath); + + /// + /// Computes the restore input hash and compares it against the cached value to determine whether a + /// restore can be skipped. + /// + internal static RestoreDecision Evaluate(IFileSystem fileSystem, IFileInfo target) + { + var projectDir = target.Directory?.FullName ?? fileSystem.Path.GetDirectoryName(target.FullName)!; + var objDir = fileSystem.Path.Combine(projectDir, "obj"); + var cacheFilePath = fileSystem.Path.Combine(objDir, CacheFileName); + + var computedHash = HashCache.ComputeHash(fileSystem, + projectDir, + CollectRestoreInputs(fileSystem, target, projectDir)); + + // A restore can only be skipped if a previous restore actually produced its output (project.assets.json) + // and the cached hash matches the current inputs. + var canSkip = fileSystem.Directory.Exists(objDir) && + fileSystem.File.Exists(fileSystem.Path.Combine(objDir, RestoreMarkerFileName)) && + fileSystem.File.Exists(cacheFilePath) && + string.Equals(HashCache.TryRead(fileSystem, cacheFilePath), + computedHash, + StringComparison.OrdinalIgnoreCase); + + return new(canSkip, computedHash, cacheFilePath); + } + + /// + /// Persists the computed hash so a subsequent run can skip the restore. Best-effort: failures are ignored. + /// + internal static void Save(IFileSystem fileSystem, RestoreDecision decision) => + HashCache.Write(fileSystem, decision.CacheFilePath, decision.ComputedHash); + + /// + /// Collects the restore-affecting files for the given target by walking up to the project root. + /// + internal static List CollectRestoreInputs(IFileSystem fileSystem, IFileInfo target, string projectDir) + { + var files = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (fileSystem.File.Exists(target.FullName)) + files.Add(target.FullName); + + var current = fileSystem.DirectoryInfo.New(projectDir); + + while (current is { Exists: true }) + { + foreach (var name in RestoreInputFileNames) + { + var candidate = fileSystem.Path.Combine(current.FullName, name); + + if (fileSystem.File.Exists(candidate)) + files.Add(candidate); + } + + // Don't walk higher than the project root. + if (FileFinder.IsRootDirectory(fileSystem, current.FullName)) + break; + + current = current.Parent; + } + + return files.ToList(); + } +} diff --git a/DecSm.Atom.Tool/RunArgsFilter.cs b/src/Invex.Atom.Tool/RunArgsFilter.cs similarity index 60% rename from DecSm.Atom.Tool/RunArgsFilter.cs rename to src/Invex.Atom.Tool/RunArgsFilter.cs index d15aac27..88ef5aa8 100644 --- a/DecSm.Atom.Tool/RunArgsFilter.cs +++ b/src/Invex.Atom.Tool/RunArgsFilter.cs @@ -1,12 +1,12 @@ -namespace DecSm.Atom.Tool; +namespace Invex.Atom.Tool; /// -/// A custom filter for the DecSm.Atom.Tool console application to handle argument parsing. +/// A custom filter for the Invex.Atom.Tool console application to handle argument parsing. /// /// /// This filter modifies the before command invocation, /// specifically adjusting the to correctly -/// parse arguments intended for the underlying DecSm.Atom project. +/// parse arguments intended for the underlying Invex.Atom project. /// It ensures that arguments following the main command (or project flag) are /// passed directly to the Atom build process. /// @@ -30,17 +30,41 @@ public override async Task InvokeAsync(ConsoleAppContext context, CancellationTo // For other commands, especially the root command that runs an Atom project, // adjust the EscapeIndex to correctly pass arguments to the Atom project. - // If the first argument is "-p" or "--project", then the project name is the second argument, - // and subsequent arguments are for the Atom project. Otherwise, all arguments are for Atom. + // Consume any leading tool-level options (the project/file selectors and the + // restore-cache flag) so everything after them is forwarded to the Atom project. + var escapeIndex = 0; + var arguments = context.Arguments; + + while (escapeIndex < arguments.Length) + { + var current = arguments[escapeIndex]; + + if (current is "-p" or "--project" or "-f" or "--file") + { + // These options take a value, so skip both the option and its value. + if (escapeIndex + 1 >= arguments.Length) + break; + + escapeIndex += 2; + } + else if (current is "--no-restore-cache") + { + // Boolean flag, skip just the flag itself. + escapeIndex += 1; + } + else + { + break; + } + } + await Next.InvokeAsync(new(context.CommandName, context.Arguments, ReadOnlyMemory.Empty, // Raw arguments are not directly used by the next layer after this filter context.State, null, context.CommandDepth, - context.Arguments[0] is "-p" or "--project" or "-f" or "--file" && context.Arguments.Length >= 2 - ? 2 // Skip "-p" or "--project" or "-f" or "--file" and the project/file name - : 0), // All arguments are for the Atom project + escapeIndex), cancellationToken); } } diff --git a/DecSm.Atom.Tool/_usings.cs b/src/Invex.Atom.Tool/_usings.cs similarity index 54% rename from DecSm.Atom.Tool/_usings.cs rename to src/Invex.Atom.Tool/_usings.cs index 27743363..275653c7 100644 --- a/DecSm.Atom.Tool/_usings.cs +++ b/src/Invex.Atom.Tool/_usings.cs @@ -1,10 +1,12 @@ global using System.Diagnostics; global using System.IO.Abstractions; global using System.Runtime.InteropServices; +global using System.Security.Cryptography; +global using System.Text; global using ConsoleAppFramework; -global using DecSm.Atom.Tool; -global using DecSm.Atom.Tool.Commands; +global using Invex.Atom.Tool; +global using Invex.Atom.Tool.Commands; global using JetBrains.Annotations; using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("DecSm.Atom.Tool.Tests")] +[assembly: InternalsVisibleTo("Invex.Atom.Tool.Tests")] diff --git a/src/Invex.Atom.Workflows/DebugWorkflow.cs b/src/Invex.Atom.Workflows/DebugWorkflow.cs new file mode 100644 index 00000000..5938d6d2 --- /dev/null +++ b/src/Invex.Atom.Workflows/DebugWorkflow.cs @@ -0,0 +1,26 @@ +namespace Invex.Atom.Workflows; + +[PublicAPI] +public sealed record DebugWorkflowType : IWorkflowType +{ + public bool IsRunning => true; +} + +[PublicAPI] +public class DebugWorkflowWriter(IRootedFileSystem fileSystem, ILogger> logger) + : WorkflowFileWriter(fileSystem, logger) +{ + private static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + WriteIndented = true, + }; + + private readonly IRootedFileSystem _fileSystem = fileSystem; + + protected override string FileExtension => "md"; + + protected override RootedPath FileLocation => _fileSystem.AtomRootDirectory / ".debug-workflows"; + + protected override string WriteWorkflow(WorkflowModel workflow) => + JsonSerializer.Serialize(workflow, JsonSerializerOptions); +} diff --git a/DecSm.Atom/Workflows/Definition/MatrixDimension.cs b/src/Invex.Atom.Workflows/Definition/MatrixDimension.cs similarity index 87% rename from DecSm.Atom/Workflows/Definition/MatrixDimension.cs rename to src/Invex.Atom.Workflows/Definition/MatrixDimension.cs index 0bdc816d..e46d98fc 100644 --- a/DecSm.Atom/Workflows/Definition/MatrixDimension.cs +++ b/src/Invex.Atom.Workflows/Definition/MatrixDimension.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition; +namespace Invex.Atom.Workflows.Definition; /// /// Represents a dimension in a build matrix, defining variations for a workflow job or step. @@ -17,5 +17,5 @@ public record MatrixDimension(string Name) /// /// These values represent the different options for this dimension (e.g., "windows-latest", "ubuntu-latest"). /// - public IReadOnlyList Values { get; init; } = []; + public TextExpressionCollection Values { get; init; } = []; } diff --git a/DecSm.Atom/Workflows/Definition/Triggers/GitPullRequestTrigger.cs b/src/Invex.Atom.Workflows/Definition/Triggers/GitPullRequestTrigger.cs similarity index 85% rename from DecSm.Atom/Workflows/Definition/Triggers/GitPullRequestTrigger.cs rename to src/Invex.Atom.Workflows/Definition/Triggers/GitPullRequestTrigger.cs index f69beea2..4d5fb843 100644 --- a/DecSm.Atom/Workflows/Definition/Triggers/GitPullRequestTrigger.cs +++ b/src/Invex.Atom.Workflows/Definition/Triggers/GitPullRequestTrigger.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition.Triggers; +namespace Invex.Atom.Workflows.Definition.Triggers; /// /// Represents a workflow trigger that activates on Git pull request events. @@ -47,12 +47,4 @@ public sealed record GitPullRequestTrigger : IWorkflowTrigger /// Gets the list of pull request event types that should activate this trigger (e.g., "opened", "synchronize"). /// public IReadOnlyList Types { get; init; } = []; - - /// - /// Gets a pre-configured trigger instance that activates on pull requests targeting the main branch. - /// - public static GitPullRequestTrigger IntoMain { get; } = new() - { - IncludedBranches = ["main"], - }; } diff --git a/DecSm.Atom/Workflows/Definition/Triggers/GitPushTrigger.cs b/src/Invex.Atom.Workflows/Definition/Triggers/GitPushTrigger.cs similarity index 87% rename from DecSm.Atom/Workflows/Definition/Triggers/GitPushTrigger.cs rename to src/Invex.Atom.Workflows/Definition/Triggers/GitPushTrigger.cs index 491a4859..3d35315d 100644 --- a/DecSm.Atom/Workflows/Definition/Triggers/GitPushTrigger.cs +++ b/src/Invex.Atom.Workflows/Definition/Triggers/GitPushTrigger.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition.Triggers; +namespace Invex.Atom.Workflows.Definition.Triggers; /// /// Represents a workflow trigger that activates when code is pushed to a Git repository. @@ -53,12 +53,4 @@ public sealed record GitPushTrigger : IWorkflowTrigger /// . /// public IReadOnlyList ExcludedTags { get; init; } = []; - - /// - /// Gets a predefined trigger that activates only on pushes to the main branch. - /// - public static GitPushTrigger ToMain { get; } = new() - { - IncludedBranches = ["main"], - }; } diff --git a/DecSm.Atom/Workflows/Definition/Triggers/IWorkflowTrigger.cs b/src/Invex.Atom.Workflows/Definition/Triggers/IWorkflowTrigger.cs similarity index 81% rename from DecSm.Atom/Workflows/Definition/Triggers/IWorkflowTrigger.cs rename to src/Invex.Atom.Workflows/Definition/Triggers/IWorkflowTrigger.cs index b12a89f8..f2050915 100644 --- a/DecSm.Atom/Workflows/Definition/Triggers/IWorkflowTrigger.cs +++ b/src/Invex.Atom.Workflows/Definition/Triggers/IWorkflowTrigger.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition.Triggers; +namespace Invex.Atom.Workflows.Definition.Triggers; /// /// Represents a trigger that can initiate a workflow. @@ -9,7 +9,6 @@ namespace DecSm.Atom.Workflows.Definition.Triggers; /// : Triggers on pull request events. /// : Triggers on code pushes to a Git repository. /// : Allows a workflow to be triggered manually. -/// : Triggers a workflow based on a CRON schedule. /// /// Workflows can be configured with a list of triggers, any of which can initiate execution. /// diff --git a/DecSm.Atom/Workflows/Definition/Triggers/ManualBoolInput.cs b/src/Invex.Atom.Workflows/Definition/Triggers/ManualBoolInput.cs similarity index 96% rename from DecSm.Atom/Workflows/Definition/Triggers/ManualBoolInput.cs rename to src/Invex.Atom.Workflows/Definition/Triggers/ManualBoolInput.cs index 30f51ab7..ebfc62f6 100644 --- a/DecSm.Atom/Workflows/Definition/Triggers/ManualBoolInput.cs +++ b/src/Invex.Atom.Workflows/Definition/Triggers/ManualBoolInput.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition.Triggers; +namespace Invex.Atom.Workflows.Definition.Triggers; /// /// Represents a manually-triggered workflow input that accepts a boolean value. diff --git a/DecSm.Atom/Workflows/Definition/Triggers/ManualChoiceInput.cs b/src/Invex.Atom.Workflows/Definition/Triggers/ManualChoiceInput.cs similarity index 96% rename from DecSm.Atom/Workflows/Definition/Triggers/ManualChoiceInput.cs rename to src/Invex.Atom.Workflows/Definition/Triggers/ManualChoiceInput.cs index 7220b1fd..4c5c6281 100644 --- a/DecSm.Atom/Workflows/Definition/Triggers/ManualChoiceInput.cs +++ b/src/Invex.Atom.Workflows/Definition/Triggers/ManualChoiceInput.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition.Triggers; +namespace Invex.Atom.Workflows.Definition.Triggers; /// /// Represents a manual input in a workflow where the user selects a value from a predefined list of choices. diff --git a/DecSm.Atom/Workflows/Definition/Triggers/ManualInput.cs b/src/Invex.Atom.Workflows/Definition/Triggers/ManualInput.cs similarity index 90% rename from DecSm.Atom/Workflows/Definition/Triggers/ManualInput.cs rename to src/Invex.Atom.Workflows/Definition/Triggers/ManualInput.cs index 0f362892..bfa99059 100644 --- a/DecSm.Atom/Workflows/Definition/Triggers/ManualInput.cs +++ b/src/Invex.Atom.Workflows/Definition/Triggers/ManualInput.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition.Triggers; +namespace Invex.Atom.Workflows.Definition.Triggers; /// /// Represents the base definition for a manual input trigger in a workflow. diff --git a/DecSm.Atom/Workflows/Definition/Triggers/ManualStringInput.cs b/src/Invex.Atom.Workflows/Definition/Triggers/ManualStringInput.cs similarity index 92% rename from DecSm.Atom/Workflows/Definition/Triggers/ManualStringInput.cs rename to src/Invex.Atom.Workflows/Definition/Triggers/ManualStringInput.cs index 850bcc46..15570508 100644 --- a/DecSm.Atom/Workflows/Definition/Triggers/ManualStringInput.cs +++ b/src/Invex.Atom.Workflows/Definition/Triggers/ManualStringInput.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition.Triggers; +namespace Invex.Atom.Workflows.Definition.Triggers; /// /// Represents a manually provided string input for a workflow. @@ -8,7 +8,7 @@ namespace DecSm.Atom.Workflows.Definition.Triggers; /// A flag indicating whether this input must be provided by the user. /// An optional default value for the string input. [PublicAPI] -public sealed record ManualStringInput(string Name, string Description, bool? Required, string? DefaultValue) +public sealed record ManualStringInput(string Name, string Description, bool? Required, string? DefaultValue = null) : ManualInput(Name, Description, Required) { /// diff --git a/DecSm.Atom/Workflows/Definition/Triggers/ManualTrigger.cs b/src/Invex.Atom.Workflows/Definition/Triggers/ManualTrigger.cs similarity index 51% rename from DecSm.Atom/Workflows/Definition/Triggers/ManualTrigger.cs rename to src/Invex.Atom.Workflows/Definition/Triggers/ManualTrigger.cs index 87a360b7..aff80995 100644 --- a/DecSm.Atom/Workflows/Definition/Triggers/ManualTrigger.cs +++ b/src/Invex.Atom.Workflows/Definition/Triggers/ManualTrigger.cs @@ -1,14 +1,8 @@ -namespace DecSm.Atom.Workflows.Definition.Triggers; +namespace Invex.Atom.Workflows.Definition.Triggers; /// /// Represents a workflow trigger that is initiated manually, optionally with a predefined set of inputs. /// /// A read-only list of manual inputs required when the workflow is triggered. [PublicAPI] -public sealed record ManualTrigger(IReadOnlyList? Inputs = null) : IWorkflowTrigger -{ - /// - /// Gets an instance of with no inputs, representing a simple manual trigger. - /// - public static ManualTrigger Empty { get; } = new(); -} +public sealed record ManualTrigger(IReadOnlyList? Inputs = null) : IWorkflowTrigger; diff --git a/src/Invex.Atom.Workflows/Definition/Triggers/WorkflowTriggers.cs b/src/Invex.Atom.Workflows/Definition/Triggers/WorkflowTriggers.cs new file mode 100644 index 00000000..21add825 --- /dev/null +++ b/src/Invex.Atom.Workflows/Definition/Triggers/WorkflowTriggers.cs @@ -0,0 +1,66 @@ +namespace Invex.Atom.Workflows.Definition.Triggers; + +[PublicAPI] +public static class WorkflowTriggers +{ + public static ManualTrigger Manual => field ??= new(); + + public static GitPullRequestTrigger PullIntoMain => + field ??= new() + { + IncludedBranches = ["main"], + }; + + public static GitPushTrigger PushToMain => + field ??= new() + { + IncludedBranches = ["main"], + }; + + public static GitPullRequestTrigger PullInto(params string[] includedBranches) => + new() + { + IncludedBranches = includedBranches.ToList(), + }; + + public static GitPushTrigger PushTo(params string[] includedBranches) => + new() + { + IncludedBranches = includedBranches.ToList(), + }; + + public static ManualTrigger ManualWithInputs(params ManualInput[] inputs) => + new(inputs.ToList()); + + public static GitPullRequestTrigger PullRequest( + IReadOnlyList includedBranches, + IReadOnlyList excludedBranches, + IReadOnlyList includedPaths, + IReadOnlyList excludedPaths, + IReadOnlyList types) => + new() + { + IncludedBranches = includedBranches, + ExcludedBranches = excludedBranches, + IncludedPaths = includedPaths, + ExcludedPaths = excludedPaths, + Types = types, + }; + + public static GitPushTrigger Push( + IReadOnlyList includedBranches, + IReadOnlyList excludedBranches, + IReadOnlyList includedPaths, + IReadOnlyList excludedPaths, + IReadOnlyList includedTags, + IReadOnlyList excludedTags) => + new() + { + IncludedBranches = includedBranches, + ExcludedBranches = excludedBranches, + IncludedPaths = includedPaths, + ExcludedPaths = excludedPaths, + IncludedTags = includedTags, + ExcludedTags = excludedTags, + }; +} diff --git a/DecSm.Atom/Workflows/Definition/WorkflowDefinition.cs b/src/Invex.Atom.Workflows/Definition/WorkflowDefinition.cs similarity index 83% rename from DecSm.Atom/Workflows/Definition/WorkflowDefinition.cs rename to src/Invex.Atom.Workflows/Definition/WorkflowDefinition.cs index 43cab778..d95a1615 100644 --- a/DecSm.Atom/Workflows/Definition/WorkflowDefinition.cs +++ b/src/Invex.Atom.Workflows/Definition/WorkflowDefinition.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition; +namespace Invex.Atom.Workflows.Definition; /// /// Represents the definition of a workflow, including its name, triggers, options, targets, and types. @@ -15,7 +15,7 @@ public sealed record WorkflowDefinition(string Name) /// /// Gets the collection of options or parameters that can be configured for the workflow. /// - public IReadOnlyList Options { get; init; } = []; + public IReadOnlyList Options { get; init; } = []; /// /// Gets the collection of targets that define the sequence of tasks to be executed by the workflow. @@ -25,5 +25,5 @@ public sealed record WorkflowDefinition(string Name) /// /// Gets the collection of workflow types that this definition applies to (e.g., GitHub Actions, Azure DevOps). /// - public IReadOnlyList WorkflowTypes { get; init; } = []; + public IReadOnlyList Types { get; init; } = []; } diff --git a/src/Invex.Atom.Workflows/Definition/WorkflowLabels.cs b/src/Invex.Atom.Workflows/Definition/WorkflowLabels.cs new file mode 100644 index 00000000..3b82ad69 --- /dev/null +++ b/src/Invex.Atom.Workflows/Definition/WorkflowLabels.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Workflows.Definition; + +[PublicAPI] +public static class WorkflowLabels; diff --git a/src/Invex.Atom.Workflows/Definition/WorkflowPresets.cs b/src/Invex.Atom.Workflows/Definition/WorkflowPresets.cs new file mode 100644 index 00000000..1bb10434 --- /dev/null +++ b/src/Invex.Atom.Workflows/Definition/WorkflowPresets.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Workflows.Definition; + +[PublicAPI] +public static class WorkflowPresets; diff --git a/src/Invex.Atom.Workflows/Definition/WorkflowSteps.cs b/src/Invex.Atom.Workflows/Definition/WorkflowSteps.cs new file mode 100644 index 00000000..9e65ebd9 --- /dev/null +++ b/src/Invex.Atom.Workflows/Definition/WorkflowSteps.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Workflows.Definition; + +[PublicAPI] +public static class WorkflowSteps; diff --git a/DecSm.Atom/Workflows/Definition/WorkflowTargetDefinition.cs b/src/Invex.Atom.Workflows/Definition/WorkflowTargetDefinition.cs similarity index 67% rename from DecSm.Atom/Workflows/Definition/WorkflowTargetDefinition.cs rename to src/Invex.Atom.Workflows/Definition/WorkflowTargetDefinition.cs index 63ffc537..ea5731a4 100644 --- a/DecSm.Atom/Workflows/Definition/WorkflowTargetDefinition.cs +++ b/src/Invex.Atom.Workflows/Definition/WorkflowTargetDefinition.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition; +namespace Invex.Atom.Workflows.Definition; /// /// Defines a single target or step within a workflow, including its configuration and behavior. @@ -15,35 +15,20 @@ public sealed record WorkflowTargetDefinition(string Name) /// /// Gets the options that configure this workflow target's behavior. /// - public IReadOnlyList Options { get; init; } = []; - - /// - /// Gets a value indicating whether artifact publishing should be suppressed for this target. - /// - public bool SuppressArtifactPublishing { get; init; } - - /// - /// Gets a new with artifact publishing suppressed. - /// - public WorkflowTargetDefinition WithSuppressedArtifactPublishing => - this with - { - SuppressArtifactPublishing = true, - }; - - public static implicit operator WorkflowTargetDefinition(string name) => - new(name); + public IReadOnlyList Options { get; init; } = []; /// /// Creates a from this target definition. /// /// A new instance. - public WorkflowStepModel CreateModel() => - new(Name) + public WorkflowStepModel CreateModel(IEnumerable workflowOptions) => + new() { + Name = Name, MatrixDimensions = MatrixDimensions, - SuppressArtifactPublishing = SuppressArtifactPublishing, - Options = Options, + Options = workflowOptions + .Concat(Options) + .ToList(), }; /// @@ -64,7 +49,7 @@ this with /// /// The options to add. /// A new instance. - public WorkflowTargetDefinition WithOptions(params IEnumerable options) => + public WorkflowTargetDefinition WithOptions(params IEnumerable options) => this with { Options = Options diff --git a/src/Invex.Atom.Workflows/Definition/WorkflowTypes.cs b/src/Invex.Atom.Workflows/Definition/WorkflowTypes.cs new file mode 100644 index 00000000..d8368058 --- /dev/null +++ b/src/Invex.Atom.Workflows/Definition/WorkflowTypes.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Workflows.Definition; + +[PublicAPI] +public static class WorkflowTypes; diff --git a/src/Invex.Atom.Workflows/Dotnet/DotnetWorkflowLabelsExtensions.cs b/src/Invex.Atom.Workflows/Dotnet/DotnetWorkflowLabelsExtensions.cs new file mode 100644 index 00000000..950e6a57 --- /dev/null +++ b/src/Invex.Atom.Workflows/Dotnet/DotnetWorkflowLabelsExtensions.cs @@ -0,0 +1,33 @@ +namespace Invex.Atom.Workflows.Dotnet; + +[PublicAPI] +public static class DotnetWorkflowLabelsExtensions +{ + [PublicAPI] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public sealed class Framework + { + public readonly string Net_10_0 = "net10.0"; + public readonly string Net_4_7_1 = "net471"; + public readonly string Net_4_8 = "net48"; + public readonly string Net_4_8_1 = "net481"; + public readonly string Net_8_0 = "net8.0"; + public readonly string Net_9_0 = "net9.0"; + public readonly string Net_Standard_2_0 = "netstandard2.0"; + public readonly string Net_Standard_2_1 = "netstandard2.1"; + } + + [PublicAPI] + public sealed class DotnetLabels + { + internal static DotnetLabels Instance => field ??= new(); + + public Framework Framework => field ??= new(); + } + + extension(WorkflowLabels) + { + [PublicAPI] + public static DotnetLabels Dotnet => DotnetLabels.Instance; + } +} diff --git a/DecSm.Atom/Nuget/AddNugetFeedsStep.cs b/src/Invex.Atom.Workflows/Dotnet/Nuget/AddNugetFeedsStep.cs similarity index 83% rename from DecSm.Atom/Nuget/AddNugetFeedsStep.cs rename to src/Invex.Atom.Workflows/Dotnet/Nuget/AddNugetFeedsStep.cs index b8f34fac..e99bf4a7 100644 --- a/DecSm.Atom/Nuget/AddNugetFeedsStep.cs +++ b/src/Invex.Atom.Workflows/Dotnet/Nuget/AddNugetFeedsStep.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Nuget; +namespace Invex.Atom.Workflows.Dotnet.Nuget; /// /// A custom workflow step that configures NuGet feeds on a build agent by adding them to the nuget.config file. @@ -8,14 +8,18 @@ /// tasks run. /// [PublicAPI] -public sealed record AddNugetFeedsStep : CustomStep +public sealed record AddNugetFeedsStep : IAdditionalStepOption { /// /// Gets the list of NuGet feeds to be added to the build agent's configuration. /// public IReadOnlyList FeedsToAdd { get; init; } = []; - public bool SyncAtomToolVersionToLibraryVersion { get; init; } + public bool SyncAtomToolVersionToLibraryVersion { get; init; } = true; + + public bool Enabled { get; init; } = true; + + public int Order { get; init; } = -100; /// /// Generates a standardized environment variable name for a given NuGet feed's authentication token. diff --git a/DecSm.Atom/Nuget/NugetFeedOptions.cs b/src/Invex.Atom.Workflows/Dotnet/Nuget/NugetFeedOptions.cs similarity index 96% rename from DecSm.Atom/Nuget/NugetFeedOptions.cs rename to src/Invex.Atom.Workflows/Dotnet/Nuget/NugetFeedOptions.cs index 4344d416..fb0c9226 100644 --- a/DecSm.Atom/Nuget/NugetFeedOptions.cs +++ b/src/Invex.Atom.Workflows/Dotnet/Nuget/NugetFeedOptions.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Nuget; +namespace Invex.Atom.Workflows.Dotnet.Nuget; /// /// Represents the configuration for a NuGet feed to be added during a build workflow. diff --git a/DecSm.Atom/Util/BuildCache.cs b/src/Invex.Atom.Workflows/Experimental/BuildCache.cs similarity index 96% rename from DecSm.Atom/Util/BuildCache.cs rename to src/Invex.Atom.Workflows/Experimental/BuildCache.cs index 9335db64..9396f162 100644 --- a/DecSm.Atom/Util/BuildCache.cs +++ b/src/Invex.Atom.Workflows/Experimental/BuildCache.cs @@ -1,10 +1,10 @@ -namespace DecSm.Atom.Util; +namespace Invex.Atom.Workflows.Experimental; /// /// Represents a cache scoped to a build, using an as the key. /// /// The type of the items to be stored in the cache. -[PublicAPI] +[UnstableAPI] public sealed class BuildCache { private readonly Dictionary _cache = []; diff --git a/src/Invex.Atom.Workflows/Experimental/WorkflowCache.cs b/src/Invex.Atom.Workflows/Experimental/WorkflowCache.cs new file mode 100644 index 00000000..196d0038 --- /dev/null +++ b/src/Invex.Atom.Workflows/Experimental/WorkflowCache.cs @@ -0,0 +1,93 @@ +namespace Invex.Atom.Workflows.Experimental; + +[UnstableAPI] +public static partial class WorkflowCacheUtil +{ + [GeneratedRegex(@"[^a-zA-Z0-9_\.]")] + public static partial Regex SanitizeRegex(); + + public static string ConvertNameToId(string name) => + SanitizeRegex() + .Replace(name, "-") + .ToLowerInvariant(); +} + +[UnstableAPI] +public sealed record WorkflowCacheSaveOption : IBuildOption +{ + public required string Name { get; init; } + + public required TextExpression Key { get; init; } + + public required TextExpressionCollection Paths { get; init; } + + public TextExpression? RunIf { get; init; } + + public bool RunOnlyIfMatchingNameCacheMissed { get; init; } = true; + + public string StepId => $"cache-save-{WorkflowCacheUtil.ConvertNameToId(Name)}"; +} + +[UnstableAPI] +public sealed record WorkflowCacheRestoreOption : IBuildOption +{ + public required string Name { get; init; } + + public required TextExpression Key { get; init; } + + public required TextExpressionCollection Paths { get; init; } + + public TextExpression? RunIf { get; init; } + + public string StepId => $"cache-restore-{WorkflowCacheUtil.ConvertNameToId(Name)}"; +} + +[UnstableAPI] +public static class WorkflowCacheOptions +{ + [UnstableAPI] + [SuppressMessage("Performance", "CA1822:Mark members as static")] + public sealed class CacheOptions + { + internal static CacheOptions Instance => field ??= new(); + + public WorkflowCacheSaveOption Save( + string name, + TextExpression key, + IEnumerable paths, + TextExpression? runIf = null, + bool runOnlyIfMatchingNameCacheMissed = true) => + new() + { + Name = name, + Key = key, + Paths = paths.ToArray() is { Length: > 0 } pathsArray + ? pathsArray + : throw new ArgumentException("At least one path must be specified."), + RunIf = runIf, + RunOnlyIfMatchingNameCacheMissed = runOnlyIfMatchingNameCacheMissed, + }; + + [UnstableAPI] + public WorkflowCacheRestoreOption Restore( + string name, + TextExpression key, + IEnumerable paths, + TextExpression? runIf = null) => + new() + { + Name = name, + Key = key, + Paths = paths.ToArray() is { Length: > 0 } pathsArray + ? pathsArray + : throw new ArgumentException("At least one path must be specified."), + RunIf = runIf, + }; + } + + extension(BuildOptions) + { + [UnstableAPI] + public static CacheOptions Cache => CacheOptions.Instance; + } +} diff --git a/src/Invex.Atom.Workflows/Extensions/AtomWorkflowsTextExpressionsExtensions.cs b/src/Invex.Atom.Workflows/Extensions/AtomWorkflowsTextExpressionsExtensions.cs new file mode 100644 index 00000000..c3c4b0ba --- /dev/null +++ b/src/Invex.Atom.Workflows/Extensions/AtomWorkflowsTextExpressionsExtensions.cs @@ -0,0 +1,26 @@ +namespace Invex.Atom.Workflows.Extensions; + +[PublicAPI] +[SuppressMessage("Performance", "CA1822:Mark members as static")] +public static class AtomWorkflowsTextExpressionsExtensions +{ + [PublicAPI] + public sealed class AtomWorkflowTextExpressions + { + internal static AtomWorkflowTextExpressions Instance => field ??= new(); + + public TargetOutputExpression ParamOutput(T buildDefinition, string targetName, string paramName) + where T : IBuildDefinition => + new() + { + TargetName = targetName, + OutputName = buildDefinition.ParamDefinitions.FirstOrDefault(p => p.Key == paramName) + .Value.ArgName, + }; + } + + extension(TextExpressions) + { + public static AtomWorkflowTextExpressions Target => AtomWorkflowTextExpressions.Instance; + } +} diff --git a/src/Invex.Atom.Workflows/IGen.cs b/src/Invex.Atom.Workflows/IGen.cs new file mode 100644 index 00000000..5e0fe21a --- /dev/null +++ b/src/Invex.Atom.Workflows/IGen.cs @@ -0,0 +1,7 @@ +namespace Invex.Atom.Workflows; + +[PublicAPI] +public interface IGen : IBuildAccessor +{ + Target Gen => t => t.DescribedAs("Generates workflow files"); +} diff --git a/DecSm.Atom/Workflows/Options/IJobRunsOn.cs b/src/Invex.Atom.Workflows/IJobRunsOn.cs similarity index 52% rename from DecSm.Atom/Workflows/Options/IJobRunsOn.cs rename to src/Invex.Atom.Workflows/IJobRunsOn.cs index a0aa7305..e5fba828 100644 --- a/DecSm.Atom/Workflows/Options/IJobRunsOn.cs +++ b/src/Invex.Atom.Workflows/IJobRunsOn.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Options; +namespace Invex.Atom.Workflows; /// /// Defines the execution environment (runner or agent) for a workflow job. @@ -8,23 +8,8 @@ /// It is typically used in build matrices to run jobs across multiple environments. /// [PublicAPI] -public interface IJobRunsOn : IBuildDefinition, IBuildAccessor +public interface IJobRunsOn : IBuildAccessor { - /// - /// Represents the tag for the latest available Windows runner. - /// - const string WindowsLatestTag = "windows-latest"; - - /// - /// Represents the tag for the latest available Ubuntu runner. - /// - const string UbuntuLatestTag = "ubuntu-latest"; - - /// - /// Represents the tag for the latest available macOS runner. - /// - const string MacOsLatestTag = "macos-latest"; - /// /// Gets the runner or agent tag for the job, sourced from the "job-runs-on" parameter. /// diff --git a/src/Invex.Atom.Workflows/IWorkflowBuildDefinition.cs b/src/Invex.Atom.Workflows/IWorkflowBuildDefinition.cs new file mode 100644 index 00000000..3bde2fb9 --- /dev/null +++ b/src/Invex.Atom.Workflows/IWorkflowBuildDefinition.cs @@ -0,0 +1,25 @@ +namespace Invex.Atom.Workflows; + +[PublicAPI] +[ConfigureHostBuilder] +[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] +public partial interface IWorkflowBuildDefinition : IBuildDefinition, IGen +{ + /// + /// Gets the collection of workflow definitions for the build. + /// + /// + /// Workflows define how targets are orchestrated, potentially across different CI/CD platforms. + /// + IReadOnlyList Workflows { get; } + + protected static partial void ConfigureBuilderFromIWorkflowBuildDefinition(IHostApplicationBuilder builder) => + builder + .Services + .AddSingleton(services => + (IWorkflowBuildDefinition)services.GetRequiredService()) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); +} diff --git a/DecSm.Atom/Workflows/Definition/IWorkflowType.cs b/src/Invex.Atom.Workflows/IWorkflowType.cs similarity index 92% rename from DecSm.Atom/Workflows/Definition/IWorkflowType.cs rename to src/Invex.Atom.Workflows/IWorkflowType.cs index ecf7566b..d06d8144 100644 --- a/DecSm.Atom/Workflows/Definition/IWorkflowType.cs +++ b/src/Invex.Atom.Workflows/IWorkflowType.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition; +namespace Invex.Atom.Workflows; /// /// Defines a contract for a workflow type, representing a specific CI/CD platform or execution environment. diff --git a/src/Invex.Atom.Workflows/Invex.Atom.Workflows.csproj b/src/Invex.Atom.Workflows/Invex.Atom.Workflows.csproj new file mode 100644 index 00000000..0bb3814f --- /dev/null +++ b/src/Invex.Atom.Workflows/Invex.Atom.Workflows.csproj @@ -0,0 +1,55 @@ + + + + net10.0;net9.0;net8.0 + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + WorkflowExpression.cs + + + WorkflowExpression.cs + + + WorkflowExpression.cs + + + WorkflowExpression.cs + + + + \ No newline at end of file diff --git a/src/Invex.Atom.Workflows/Invex.Atom.Workflows.props b/src/Invex.Atom.Workflows/Invex.Atom.Workflows.props new file mode 100644 index 00000000..5484eaf3 --- /dev/null +++ b/src/Invex.Atom.Workflows/Invex.Atom.Workflows.props @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Invex.Atom.Workflows/Model/WorkflowJobModel.cs b/src/Invex.Atom.Workflows/Model/WorkflowJobModel.cs new file mode 100644 index 00000000..b1ed4db0 --- /dev/null +++ b/src/Invex.Atom.Workflows/Model/WorkflowJobModel.cs @@ -0,0 +1,19 @@ +namespace Invex.Atom.Workflows.Model; + +/// +/// Represents a job within a workflow, including its name, steps, dependencies, and configuration options. +/// +[PublicAPI] +public sealed record WorkflowJobModel +{ + /// The name of the job. + public required string Name { get; init; } + + /// The sequence of steps to be executed in the job. + public required WorkflowStepModel TargetStep { get; init; } + + /// + /// Gets the names of other jobs that must be completed before this job can start. + /// + public required IReadOnlyList JobDependencies { get; init; } +} diff --git a/DecSm.Atom/Workflows/Model/WorkflowModel.cs b/src/Invex.Atom.Workflows/Model/WorkflowModel.cs similarity index 78% rename from DecSm.Atom/Workflows/Model/WorkflowModel.cs rename to src/Invex.Atom.Workflows/Model/WorkflowModel.cs index b40f351b..58850154 100644 --- a/DecSm.Atom/Workflows/Model/WorkflowModel.cs +++ b/src/Invex.Atom.Workflows/Model/WorkflowModel.cs @@ -1,13 +1,15 @@ -namespace DecSm.Atom.Workflows.Model; +namespace Invex.Atom.Workflows.Model; /// /// Represents a workflow model, including its name, triggers, options, and jobs, used for generating CI/CD workflow /// files. /// -/// The name of the workflow. [PublicAPI] -public sealed record WorkflowModel(string Name) +public sealed record WorkflowModel { + /// The name of the workflow. + public required string Name { get; init; } + /// /// Gets the triggers that define when the workflow should be initiated. /// @@ -22,7 +24,7 @@ public sealed record WorkflowModel(string Name) /// /// Options can include input parameters, environment variables, or other settings. /// - public required IReadOnlyList Options { get; init; } + public required IReadOnlyList Options { get; init; } /// /// Gets the jobs that define the sequence of tasks to be executed by the workflow. diff --git a/src/Invex.Atom.Workflows/Model/WorkflowStepModel.cs b/src/Invex.Atom.Workflows/Model/WorkflowStepModel.cs new file mode 100644 index 00000000..3c8d5acb --- /dev/null +++ b/src/Invex.Atom.Workflows/Model/WorkflowStepModel.cs @@ -0,0 +1,24 @@ +namespace Invex.Atom.Workflows.Model; + +/// +/// Represents a single step within a workflow job, defining its configuration and behavior. +/// +[PublicAPI] +public sealed record WorkflowStepModel +{ + /// The name of the workflow step. + public required string Name { get; init; } + + /// + /// Gets the matrix dimensions for running this step in multiple configurations. + /// + /// + /// A matrix allows a step to be executed multiple times with different configurations. + /// + public required IReadOnlyList MatrixDimensions { get; init; } + + /// + /// Gets the options that configure this step's behavior. + /// + public required IReadOnlyList Options { get; init; } +} diff --git a/src/Invex.Atom.Workflows/Options/CheckoutStep.cs b/src/Invex.Atom.Workflows/Options/CheckoutStep.cs new file mode 100644 index 00000000..645d6146 --- /dev/null +++ b/src/Invex.Atom.Workflows/Options/CheckoutStep.cs @@ -0,0 +1,9 @@ +namespace Invex.Atom.Workflows.Options; + +[PublicAPI] +public record CheckoutStep : IAdditionalStepOption +{ + public bool Enabled { get; init; } = true; + + public int Order { get; init; } = -1000; +} diff --git a/src/Invex.Atom.Workflows/Options/DeployToEnvironment.cs b/src/Invex.Atom.Workflows/Options/DeployToEnvironment.cs new file mode 100644 index 00000000..f38ed052 --- /dev/null +++ b/src/Invex.Atom.Workflows/Options/DeployToEnvironment.cs @@ -0,0 +1,7 @@ +namespace Invex.Atom.Workflows.Options; + +/// +/// A workflow option that specifies the name of the environment to deploy to. +/// +[PublicAPI] +public sealed record DeployToEnvironment(TextExpression EnvironmentName) : IBuildOption; diff --git a/src/Invex.Atom.Workflows/Options/IAdditionalStepOption.cs b/src/Invex.Atom.Workflows/Options/IAdditionalStepOption.cs new file mode 100644 index 00000000..f37ba008 --- /dev/null +++ b/src/Invex.Atom.Workflows/Options/IAdditionalStepOption.cs @@ -0,0 +1,18 @@ +namespace Invex.Atom.Workflows.Options; + +[PublicAPI] +public interface IAdditionalStepOption : IBuildOption +{ + /// + /// Indicates whether the step is enabled. + /// When set to true, the step is processed as part of the workflow; otherwise, it is ignored. + /// + bool Enabled { get; } + + /// + /// The order that the step should be executed within the job. + /// Values less than 0 are run before the target, and values greater than 0 are run after the target. + /// A value of 0 is invalid. + /// + int Order { get; } +} diff --git a/src/Invex.Atom.Workflows/Options/IImplicitTargetDependencyOption.cs b/src/Invex.Atom.Workflows/Options/IImplicitTargetDependencyOption.cs new file mode 100644 index 00000000..7593fc91 --- /dev/null +++ b/src/Invex.Atom.Workflows/Options/IImplicitTargetDependencyOption.cs @@ -0,0 +1,7 @@ +namespace Invex.Atom.Workflows.Options; + +[PublicAPI] +public interface IImplicitTargetDependencyOption +{ + IEnumerable TargetNames { get; } +} diff --git a/src/Invex.Atom.Workflows/Options/Injections/WorkflowParamInjection.cs b/src/Invex.Atom.Workflows/Options/Injections/WorkflowParamInjection.cs new file mode 100644 index 00000000..c4ae0ce4 --- /dev/null +++ b/src/Invex.Atom.Workflows/Options/Injections/WorkflowParamInjection.cs @@ -0,0 +1,21 @@ +namespace Invex.Atom.Workflows.Options.Injections; + +/// +/// Represents a workflow option that injects a parameter value into the workflow execution context. +/// +/// The name of the parameter to inject. +/// The value to inject for the specified parameter. +/// +/// This allows workflows to set specific parameter values that take precedence over other sources +/// like command-line arguments or environment variables. +/// +/// +/// +/// // Inject a dry-run parameter +/// var dryRunInjection = new WorkflowParamInjection("NugetDryRun", "true"); +/// // Add to workflow configuration +/// var workflowDefinition = new WorkflowDefinition().WithAddedOptions(dryRunInjection); +/// +/// +[PublicAPI] +public sealed record WorkflowParamInjection(string Name, TextExpression InjectionExpression) : IBuildOption; diff --git a/DecSm.Atom/Workflows/Definition/Options/WorkflowEnvironmentInjection.cs b/src/Invex.Atom.Workflows/Options/Injections/WorkflowParamInjectionFromEnvironment.cs similarity index 65% rename from DecSm.Atom/Workflows/Definition/Options/WorkflowEnvironmentInjection.cs rename to src/Invex.Atom.Workflows/Options/Injections/WorkflowParamInjectionFromEnvironment.cs index 7e4ffc37..13795ecd 100644 --- a/DecSm.Atom/Workflows/Definition/Options/WorkflowEnvironmentInjection.cs +++ b/src/Invex.Atom.Workflows/Options/Injections/WorkflowParamInjectionFromEnvironment.cs @@ -1,11 +1,11 @@ -namespace DecSm.Atom.Workflows.Definition.Options; +namespace Invex.Atom.Workflows.Options.Injections; /// /// Represents a workflow option that injects an environment variable into the workflow execution context. /// /// /// This allows workflows to access custom environment variables that are not part of the default runtime environment. -/// For sensitive values, consider using . +/// For sensitive values, consider using . /// /// /// @@ -16,10 +16,7 @@ /// /// [PublicAPI] -public sealed record WorkflowEnvironmentInjection : WorkflowOption -{ - /// - /// Gets a value indicating that multiple instances of this option are allowed. - /// - public override bool AllowMultiple => true; -} +public sealed record WorkflowParamInjectionFromEnvironment(string Value) : IBuildOption; + +[PublicAPI] +public sealed record WorkflowEnvironmentVariableInjection(string Name, TextExpression Value) : IBuildOption; diff --git a/DecSm.Atom/Workflows/Definition/Options/WorkflowSecretInjection.cs b/src/Invex.Atom.Workflows/Options/Injections/WorkflowSecretInjection.cs similarity index 68% rename from DecSm.Atom/Workflows/Definition/Options/WorkflowSecretInjection.cs rename to src/Invex.Atom.Workflows/Options/Injections/WorkflowSecretInjection.cs index 0eda49f8..cc52f781 100644 --- a/DecSm.Atom/Workflows/Definition/Options/WorkflowSecretInjection.cs +++ b/src/Invex.Atom.Workflows/Options/Injections/WorkflowSecretInjection.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition.Options; +namespace Invex.Atom.Workflows.Options.Injections; /// /// Represents a workflow option that injects a secret value into the workflow execution context. @@ -16,10 +16,4 @@ /// /// [PublicAPI] -public sealed record WorkflowSecretInjection : WorkflowOption -{ - /// - /// Gets a value indicating that multiple instances of this option are allowed. - /// - public override bool AllowMultiple => true; -} +public sealed record WorkflowSecretInjection(string Value) : IBuildOption; diff --git a/DecSm.Atom/Workflows/Definition/Options/WorkflowSecretsSecretInjection.cs b/src/Invex.Atom.Workflows/Options/Injections/WorkflowSecretInjectionForSecretProvider.cs similarity index 74% rename from DecSm.Atom/Workflows/Definition/Options/WorkflowSecretsSecretInjection.cs rename to src/Invex.Atom.Workflows/Options/Injections/WorkflowSecretInjectionForSecretProvider.cs index 370f76e3..7044e3a4 100644 --- a/DecSm.Atom/Workflows/Definition/Options/WorkflowSecretsSecretInjection.cs +++ b/src/Invex.Atom.Workflows/Options/Injections/WorkflowSecretInjectionForSecretProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition.Options; +namespace Invex.Atom.Workflows.Options.Injections; /// /// Represents a workflow option for injecting a secret value that is specifically intended for @@ -19,10 +19,4 @@ /// /// [PublicAPI] -public sealed record WorkflowSecretsSecretInjection : WorkflowOption -{ - /// - /// Gets a value indicating that multiple instances of this option are allowed. - /// - public override bool AllowMultiple => true; -} +public sealed record WorkflowSecretInjectionForSecretProvider(string SecretName) : IBuildOption; diff --git a/DecSm.Atom/Workflows/Definition/Options/WorkflowSecretsEnvironmentInjection.cs b/src/Invex.Atom.Workflows/Options/Injections/WorkflowSecretsInjectionFromEnvironment.cs similarity index 67% rename from DecSm.Atom/Workflows/Definition/Options/WorkflowSecretsEnvironmentInjection.cs rename to src/Invex.Atom.Workflows/Options/Injections/WorkflowSecretsInjectionFromEnvironment.cs index b6b846c5..8755ca50 100644 --- a/DecSm.Atom/Workflows/Definition/Options/WorkflowSecretsEnvironmentInjection.cs +++ b/src/Invex.Atom.Workflows/Options/Injections/WorkflowSecretsInjectionFromEnvironment.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Definition.Options; +namespace Invex.Atom.Workflows.Options.Injections; /// /// Represents a workflow option for injecting secrets into the environment during workflow execution. @@ -16,10 +16,4 @@ /// /// [PublicAPI] -public sealed record WorkflowSecretsEnvironmentInjection : WorkflowOption -{ - /// - /// Gets a value indicating that multiple instances of this option are allowed. - /// - public override bool AllowMultiple => true; -} +public sealed record WorkflowSecretsInjectionFromEnvironment(string SecretName) : IBuildOption; diff --git a/DecSm.Atom/Workflows/Options/SetupDotnetStep.cs b/src/Invex.Atom.Workflows/Options/SetupDotnetStep.cs similarity index 68% rename from DecSm.Atom/Workflows/Options/SetupDotnetStep.cs rename to src/Invex.Atom.Workflows/Options/SetupDotnetStep.cs index bbd31dd4..22709bf0 100644 --- a/DecSm.Atom/Workflows/Options/SetupDotnetStep.cs +++ b/src/Invex.Atom.Workflows/Options/SetupDotnetStep.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Options; +namespace Invex.Atom.Workflows.Options; /// /// Represents a workflow step that sets up a specific .NET SDK version. @@ -9,9 +9,15 @@ /// /// The quality of the .NET SDK to install (e.g., Preview, GA). If null, the default quality is used. /// +/// Whether to cache the installed .NET SDK. +/// The path to the project's lock file (e.g., "**/packages.lock.json") [PublicAPI] -public sealed record SetupDotnetStep(string? DotnetVersion = null, SetupDotnetStep.DotnetQuality? Quality = null) - : CustomStep +public sealed record SetupDotnetStep( + TextExpression? DotnetVersion = null, + SetupDotnetStep.DotnetQuality? Quality = null, + bool Cache = false, + string? LockFile = null +) : IAdditionalStepOption { /// /// Specifies the quality of the .NET SDK version to install. @@ -44,4 +50,8 @@ public enum DotnetQuality /// Ga, } + + public bool Enabled { get; init; } = true; + + public int Order { get; init; } = -200; } diff --git a/src/Invex.Atom.Workflows/Options/SuppressArtifactPublishingOption.cs b/src/Invex.Atom.Workflows/Options/SuppressArtifactPublishingOption.cs new file mode 100644 index 00000000..05807011 --- /dev/null +++ b/src/Invex.Atom.Workflows/Options/SuppressArtifactPublishingOption.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.Workflows.Options; + +[PublicAPI] +public sealed record SuppressArtifactPublishingOption : ToggleBuildOption; diff --git a/src/Invex.Atom.Workflows/Options/TargetCondition.cs b/src/Invex.Atom.Workflows/Options/TargetCondition.cs new file mode 100644 index 00000000..eb0a0f1b --- /dev/null +++ b/src/Invex.Atom.Workflows/Options/TargetCondition.cs @@ -0,0 +1,11 @@ +namespace Invex.Atom.Workflows.Options; + +[PublicAPI] +public sealed record TargetCondition(TextExpression Condition) : IBuildOption, IImplicitTargetDependencyOption +{ + public IEnumerable TargetNames => + TextExpressionUtils + .Flatten(Condition) + .OfType() + .Select(x => x.TargetName); +} diff --git a/src/Invex.Atom.Workflows/Options/TargetStepCondition.cs b/src/Invex.Atom.Workflows/Options/TargetStepCondition.cs new file mode 100644 index 00000000..0c560d50 --- /dev/null +++ b/src/Invex.Atom.Workflows/Options/TargetStepCondition.cs @@ -0,0 +1,11 @@ +namespace Invex.Atom.Workflows.Options; + +[PublicAPI] +public sealed record TargetStepCondition(TextExpression Condition) : IBuildOption, IImplicitTargetDependencyOption +{ + public IEnumerable TargetNames => + TextExpressionUtils + .Flatten(Condition) + .OfType() + .Select(x => x.TargetName); +} diff --git a/DecSm.Atom/Artifacts/UseCustomArtifactProvider.cs b/src/Invex.Atom.Workflows/Options/UseCustomArtifactProvider.cs similarity index 85% rename from DecSm.Atom/Artifacts/UseCustomArtifactProvider.cs rename to src/Invex.Atom.Workflows/Options/UseCustomArtifactProvider.cs index 910c8fb5..50670003 100644 --- a/DecSm.Atom/Artifacts/UseCustomArtifactProvider.cs +++ b/src/Invex.Atom.Workflows/Options/UseCustomArtifactProvider.cs @@ -1,11 +1,11 @@ -namespace DecSm.Atom.Artifacts; +namespace Invex.Atom.Workflows.Options; /// /// A workflow option that enables a custom for artifact management. /// /// /// -/// When , the workflow will use the +/// When , the workflow will use the /// and targets, /// which delegate to the registered implementation. /// @@ -37,4 +37,4 @@ /// /// [PublicAPI] -public sealed record UseCustomArtifactProvider : ToggleWorkflowOption; +public sealed record UseCustomArtifactProvider : ToggleBuildOption; diff --git a/src/Invex.Atom.Workflows/Options/WorkflowBuildOptionsExtensions.cs b/src/Invex.Atom.Workflows/Options/WorkflowBuildOptionsExtensions.cs new file mode 100644 index 00000000..287c236a --- /dev/null +++ b/src/Invex.Atom.Workflows/Options/WorkflowBuildOptionsExtensions.cs @@ -0,0 +1,160 @@ +namespace Invex.Atom.Workflows.Options; + +[PublicAPI] +[SuppressMessage("Performance", "CA1822:Mark members as static")] +public static class WorkflowBuildOptionsExtensions +{ + [PublicAPI] + public sealed class InjectionBuildOptions + { + internal static InjectionBuildOptions Instance { get; } = new(); + + public WorkflowParamInjection Param(string paramName, TextExpression injectionExpression) => + new(paramName, injectionExpression); + + public WorkflowParamInjection Param(ParamDefinition paramDefinition, TextExpression injectionExpression) => + new(paramDefinition, injectionExpression); + + public WorkflowParamInjectionFromEnvironment ParamFromWorkflowEnvironment(string name) => + new(name); + + public WorkflowEnvironmentVariableInjection EnvironmentVariable(string name, TextExpression value) => + new(name, value); + + public WorkflowSecretInjection Secret(string secretName) => + new(secretName); + + public WorkflowSecretInjection Secret(ParamDefinition secretDefinition) => + new(secretDefinition); + + public WorkflowSecretInjectionForSecretProvider SecretForSecretProvider(string secretName) => + new(secretName); + + public WorkflowSecretInjectionForSecretProvider SecretForSecretProvider(ParamDefinition secretDefinition) => + new(secretDefinition); + + public WorkflowSecretsInjectionFromEnvironment SecretFromWorkflowEnvironment(string secretName) => + new(secretName); + + public WorkflowSecretsInjectionFromEnvironment SecretFromWorkflowEnvironment( + ParamDefinition secretDefinition) => + new(secretDefinition); + } + + [PublicAPI] + public sealed class ArtifactBuildOptions + { + internal static ArtifactBuildOptions Instance { get; } = new(); + + public UseCustomArtifactProvider UseCustomProvider => + new() + { + Enabled = true, + }; + } + + [PublicAPI] + public sealed class DeploymentBuildOptions + { + internal static DeploymentBuildOptions Instance { get; } = new(); + + public DeployToEnvironment ToEnvironment(TextExpression environmentName) => + new(environmentName); + } + + [PublicAPI] + public sealed class TargetBuildOptions + { + internal static TargetBuildOptions Instance { get; } = new(); + + public SuppressArtifactPublishingOption SuppressArtifactPublishing => + new() + { + Enabled = true, + }; + + public SuppressArtifactPublishingOption SetSuppressedArtifactPublishing(bool value) => + new() + { + Enabled = value, + }; + + public TargetCondition RunIfWorkflowCondition(TextExpression condition) => + new(condition); + } + + [PublicAPI] + public sealed class StepsBuildOptions + { + internal static StepsBuildOptions Instance { get; } = new(); + + [PublicAPI] + public SetupDotnetOptions SetupDotnet => field ??= new(); + + [PublicAPI] + public AddNugetFeedsOptions AddNugetFeeds => field ??= new(); + + [PublicAPI] + public class SetupDotnetOptions + { + public SetupDotnetStep Dotnet80X(bool cache = false, string? lockFile = null) => + new("8.0.x") + { + Cache = cache, + LockFile = lockFile, + }; + + public SetupDotnetStep Dotnet90X(bool cache = false, string? lockFile = null) => + new("9.0.x") + { + Cache = cache, + LockFile = lockFile, + }; + + public SetupDotnetStep Dotnet100X(bool cache = false, string? lockFile = null) => + new("10.0.x") + { + Cache = cache, + LockFile = lockFile, + }; + + public SetupDotnetStep From( + TextExpression? dotnetVersion = null, + SetupDotnetStep.DotnetQuality? quality = null, + bool cache = false, + string? lockFile = null) => + new(dotnetVersion, quality, cache, lockFile); + } + + [PublicAPI] + public sealed class AddNugetFeedsOptions + { + public AddNugetFeedsStep AddNugetFeeds( + IEnumerable feedsToAdd, + bool syncAtomToolVersionToLibraryVersion = true) => + new() + { + FeedsToAdd = feedsToAdd.ToList(), + SyncAtomToolVersionToLibraryVersion = syncAtomToolVersionToLibraryVersion, + }; + } + } + + extension(BuildOptions) + { + [PublicAPI] + public static InjectionBuildOptions Inject => InjectionBuildOptions.Instance; + + [PublicAPI] + public static ArtifactBuildOptions Artifacts => ArtifactBuildOptions.Instance; + + [PublicAPI] + public static DeploymentBuildOptions Deploy => DeploymentBuildOptions.Instance; + + [PublicAPI] + public static TargetBuildOptions Target => TargetBuildOptions.Instance; + + [PublicAPI] + public static StepsBuildOptions Steps => StepsBuildOptions.Instance; + } +} diff --git a/src/Invex.Atom.Workflows/WorkflowBuildDefinition.cs b/src/Invex.Atom.Workflows/WorkflowBuildDefinition.cs new file mode 100644 index 00000000..db2e951b --- /dev/null +++ b/src/Invex.Atom.Workflows/WorkflowBuildDefinition.cs @@ -0,0 +1,9 @@ +namespace Invex.Atom.Workflows; + +[PublicAPI] +public abstract class WorkflowBuildDefinition(IServiceProvider services) + : BuildDefinition(services), IWorkflowBuildDefinition +{ + /// + public abstract IReadOnlyList Workflows { get; } +} diff --git a/src/Invex.Atom.Workflows/WorkflowContext/IWorkflowContextProvider.cs b/src/Invex.Atom.Workflows/WorkflowContext/IWorkflowContextProvider.cs new file mode 100644 index 00000000..97160b12 --- /dev/null +++ b/src/Invex.Atom.Workflows/WorkflowContext/IWorkflowContextProvider.cs @@ -0,0 +1,9 @@ +namespace Invex.Atom.Workflows.WorkflowContext; + +[PublicAPI] +public interface IWorkflowContextProvider +{ + IWorkflowType? WorkflowType { get; } + + string? WorkflowName { get; } +} diff --git a/src/Invex.Atom.Workflows/WorkflowContext/WorkflowContext.cs b/src/Invex.Atom.Workflows/WorkflowContext/WorkflowContext.cs new file mode 100644 index 00000000..5008f5cf --- /dev/null +++ b/src/Invex.Atom.Workflows/WorkflowContext/WorkflowContext.cs @@ -0,0 +1,26 @@ +namespace Invex.Atom.Workflows.WorkflowContext; + +[PublicAPI] +public interface IWorkflowContext +{ + IWorkflowType? WorkflowType { get; } + + string? WorkflowName { get; } +} + +internal sealed class WorkflowContext(IEnumerable providers) : IWorkflowContext +{ + private readonly IWorkflowContextProvider[] _providers = providers.ToArray(); + + public IWorkflowType? WorkflowType => + field ??= _providers + .Select(x => x.WorkflowType) + .OfType() + .FirstOrDefault(); + + public string? WorkflowName => + field ??= _providers + .Select(x => x.WorkflowName) + .OfType() + .FirstOrDefault(); +} diff --git a/DecSm.Atom/Workflows/WorkflowGenerator.cs b/src/Invex.Atom.Workflows/WorkflowGenerator.cs similarity index 63% rename from DecSm.Atom/Workflows/WorkflowGenerator.cs rename to src/Invex.Atom.Workflows/WorkflowGenerator.cs index 1278ade2..5e014e92 100644 --- a/DecSm.Atom/Workflows/WorkflowGenerator.cs +++ b/src/Invex.Atom.Workflows/WorkflowGenerator.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows; +namespace Invex.Atom.Workflows; /// /// Generates workflow files based on the defined s. @@ -6,10 +6,12 @@ /// The build definition containing workflow configurations. /// A collection of available workflow writers. /// The resolver for transforming workflow definitions into models. +/// The logger for diagnostics. internal sealed class WorkflowGenerator( - IBuildDefinition buildDefinition, + IWorkflowBuildDefinition buildDefinition, IEnumerable writers, - WorkflowResolver workflowResolver + WorkflowResolver workflowResolver, + ILogger logger ) { private readonly List _writers = writers.ToList(); @@ -20,19 +22,28 @@ WorkflowResolver workflowResolver /// A cancellation token. public async Task GenerateWorkflows(CancellationToken cancellationToken = default) { - var workflowDefinitions = ((MinimalBuildDefinition)buildDefinition).Workflows; + var workflowDefinitions = buildDefinition.Workflows; var generationTasks = new List(); // ReSharper disable LoopCanBeConvertedToQuery foreach (var workflowDefinition in workflowDefinitions) - foreach (var workflowType in workflowDefinition.WorkflowTypes) + foreach (var workflowType in workflowDefinition.Types) { // ReSharper restore LoopCanBeConvertedToQuery var writer = _writers.FirstOrDefault(w => w.WorkflowType == workflowType.GetType()); if (writer is null) + { + logger.LogWarning( + "No workflow writer found for workflow type {WorkflowType} in workflow {WorkflowName}. " + + "The workflow will not be generated. Ensure the appropriate module (e.g., Invex.Atom.Module.GithubWorkflows) is referenced.", + workflowType.GetType() + .Name, + workflowDefinition.Name); + continue; + } var workflow = workflowResolver.Resolve(workflowDefinition); var generateTask = writer.Generate(workflow, cancellationToken); @@ -44,19 +55,19 @@ public async Task GenerateWorkflows(CancellationToken cancellationToken = defaul } /// - /// Checks if any of the defined workflow files are "dirty" (i.e., need to be regenerated). + /// Checks if any of the defined workflow files are outdated (i.e., need to be regenerated). /// /// A cancellation token. - /// true if any workflow file is dirty; otherwise, false. - public async Task WorkflowsDirty(CancellationToken cancellationToken = default) + /// true if any workflow file is outdated; otherwise, false. + public async Task WorkflowsOutdated(CancellationToken cancellationToken = default) { - var workflowDefinitions = ((MinimalBuildDefinition)buildDefinition).Workflows; + var workflowDefinitions = buildDefinition.Workflows; var checkTasks = new List>(); // ReSharper disable LoopCanBeConvertedToQuery foreach (var workflowDefinition in workflowDefinitions) - foreach (var workflowType in workflowDefinition.WorkflowTypes) + foreach (var workflowType in workflowDefinition.Types) { // ReSharper restore LoopCanBeConvertedToQuery var writer = _writers.FirstOrDefault(w => w.WorkflowType == workflowType.GetType()); @@ -65,7 +76,7 @@ public async Task WorkflowsDirty(CancellationToken cancellationToken = def continue; var workflow = workflowResolver.Resolve(workflowDefinition); - var checkTask = writer.CheckForDirtyWorkflow(workflow, cancellationToken); + var checkTask = writer.CheckForOutdatedWorkflow(workflow, cancellationToken); checkTasks.Add(checkTask); } diff --git a/src/Invex.Atom.Workflows/WorkflowLifecycleHook.cs b/src/Invex.Atom.Workflows/WorkflowLifecycleHook.cs new file mode 100644 index 00000000..489d2b2c --- /dev/null +++ b/src/Invex.Atom.Workflows/WorkflowLifecycleHook.cs @@ -0,0 +1,36 @@ +namespace Invex.Atom.Workflows; + +/// +/// A lifecycle hook that manages workflow generation and outdated workflow detection. +/// +/// +/// +/// When running in non-headless mode or when the --gen flag is provided, this hook +/// will automatically generate workflow files before build targets are executed. +/// +/// +/// When running in headless mode (CI/CD), if workflows are found to be outdated, +/// a is thrown to fail the build early, +/// prompting the developer to regenerate workflows locally. +/// +/// +internal sealed class WorkflowLifecycleHook( + CommandLineArgs args, + WorkflowGenerator workflowGenerator, + ILogger logger +) : IAtomLifecycleHook +{ + public async Task BeforeExecute(CancellationToken cancellationToken) + { + if (args.Commands.Any(x => x.Name is nameof(IGen.Gen)) || !args.HasHeadless) + { + logger.LogDebug("Generating workflow files"); + await workflowGenerator.GenerateWorkflows(cancellationToken); + } + else if (await workflowGenerator.WorkflowsOutdated(cancellationToken)) + { + throw new WorkflowOutdatedException( + "One or more workflows are out of date. To regenerate workflows, run the build with the --gen flag."); + } + } +} diff --git a/src/Invex.Atom.Workflows/WorkflowOutdatedException.cs b/src/Invex.Atom.Workflows/WorkflowOutdatedException.cs new file mode 100644 index 00000000..1b12de75 --- /dev/null +++ b/src/Invex.Atom.Workflows/WorkflowOutdatedException.cs @@ -0,0 +1,71 @@ +namespace Invex.Atom.Workflows; + +/// +/// Thrown when workflow files are out of date in headless mode. +/// +/// +/// +/// This exception is thrown when the build is running in headless mode (typically in CI/CD environments) +/// and the workflow files are detected to be out of date. In headless mode, the build does not automatically +/// regenerate workflow files to prevent unexpected changes in automated environments. +/// +/// +/// Headless mode is typically enabled with the --headless flag and is used in CI/CD pipelines +/// to ensure reproducibility and prevent unintended modifications to workflow files. +/// +/// +/// To resolve this error, run the build with the --gen flag to regenerate the workflow files, +/// then commit the updated files to your repository. +/// +/// +/// +/// Typical CI/CD failure scenario and resolution: +/// +/// // In CI/CD pipeline, the build fails with: +/// // "One or more workflows are out of date. To regenerate workflows, run the build with the --gen flag." +/// +/// // To fix locally: +/// // 1. Run: dotnet run --project ./Build --gen +/// // 2. Commit the regenerated workflow files +/// // 3. Push to repository +/// +/// // Example of catching this exception: +/// try +/// { +/// await atomService.RunAsync(); +/// } +/// catch (WorkflowOutdatedException ex) +/// { +/// Console.WriteLine($"Workflows need regeneration: {ex.Message}"); +/// Console.WriteLine("Run the build with the --gen flag to regenerate workflows."); +/// Environment.ExitCode = 3; +/// } +/// +/// +/// +[PublicAPI] +[Serializable] +public class WorkflowOutdatedException : AtomException +{ + /// + /// Initializes a new instance of the class. + /// + public WorkflowOutdatedException() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public WorkflowOutdatedException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or null if no inner exception is + /// specified. + /// + public WorkflowOutdatedException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/DecSm.Atom/Workflows/WorkflowResolver.cs b/src/Invex.Atom.Workflows/WorkflowResolver.cs similarity index 64% rename from DecSm.Atom/Workflows/WorkflowResolver.cs rename to src/Invex.Atom.Workflows/WorkflowResolver.cs index 262dcee6..d99cf3c7 100644 --- a/DecSm.Atom/Workflows/WorkflowResolver.cs +++ b/src/Invex.Atom.Workflows/WorkflowResolver.cs @@ -1,19 +1,17 @@ -namespace DecSm.Atom.Workflows; +namespace Invex.Atom.Workflows; /// /// Resolves a into a fully structured . /// -/// The build definition containing global options and target information. /// The resolved build model. -/// A collection of workflow option providers. +/// The service providing the resolved set of build options. +/// The logger for diagnostics. internal sealed class WorkflowResolver( - IBuildDefinition buildDefinition, BuildModel buildModel, - IEnumerable workflowOptionProviders + IBuildOptionService buildOptionService, + ILogger logger ) { - private readonly IReadOnlyList _workflowOptionProviders = workflowOptionProviders.ToList(); - /// /// Resolves a into a , /// including job and step ordering, and dependency resolution. @@ -25,18 +23,18 @@ IEnumerable workflowOptionProviders /// public WorkflowModel Resolve(WorkflowDefinition definition) { - // Get all default options from BuildDefinition, WorkflowOptionProviders and WorkflowDefinition - var workflowOptions = IWorkflowOption - .Merge(buildDefinition - .GlobalWorkflowOptions - .Concat(_workflowOptionProviders.SelectMany(provider => provider.WorkflowOptions)) - .Concat(definition.Options)) + // Get all default options from BuildOptionService (includes IBuildDefinition.BuildOptions + providers), + // plus GlobalWorkflowOptions from the build definition and the workflow-level options + var workflowOptions = buildOptionService + .Options + .Concat(definition.Options) .ToList(); // If there are no steps, we can return a simple workflow if (definition.Targets.Count is 0) - return new(definition.Name) + return new() { + Name = definition.Name, Triggers = definition.Triggers, Options = workflowOptions, Jobs = [], @@ -45,14 +43,14 @@ public WorkflowModel Resolve(WorkflowDefinition definition) // Transform all step definitions into steps var definedSteps = definition .Targets - .Select(targetDefinition => targetDefinition.CreateModel()) + .Select(targetDefinition => targetDefinition.CreateModel(workflowOptions)) .ToList(); // Turn command steps into jobs - var definedCommandJobs = definedSteps.ConvertAll(step => new WorkflowJobModel(step.Name, [step]) + var definedCommandJobs = definedSteps.ConvertAll(step => new WorkflowJobModel { - Options = step.Options, - MatrixDimensions = step.MatrixDimensions, + Name = step.Name, + TargetStep = step, JobDependencies = buildModel .GetTarget(step.Name) .Dependencies @@ -64,19 +62,25 @@ public WorkflowModel Resolve(WorkflowDefinition definition) // consume or produce artifacts are dependent on the Setup step. // It will be up to the WorkflowWriter to implement the download/upload steps. if (UseCustomArtifactProvider.IsEnabled(workflowOptions)) - definedCommandJobs = definedCommandJobs.ConvertAll(job => job - .Steps - .Where(step => step is { SuppressArtifactPublishing: false }) - .Select(step => buildModel.GetTarget(step.Name)) - .Any(target => target.ConsumedArtifacts.Count > 0 || target.ProducedArtifacts.Count > 0) - ? job with - { - JobDependencies = job - .JobDependencies - .Append(nameof(ISetupBuildInfo.SetupBuildInfo)) - .ToList(), - } - : job); + definedCommandJobs = definedCommandJobs.ConvertAll(job => + { + var suppressArtifactPublishing = SuppressArtifactPublishingOption.IsEnabled(job.TargetStep.Options); + + if (suppressArtifactPublishing) + return job; + + if (buildModel.GetTarget(job.TargetStep.Name) is { ConsumedArtifacts.Count: > 0 } + or { ProducedArtifacts.Count: > 0 }) + return job with + { + JobDependencies = job + .JobDependencies + .Append(nameof(ISetupBuildInfo.SetupBuildInfo)) + .ToList(), + }; + + return job; + }); // Check that all consumed variables are produced by the target they are consumed from to avoid errors later on foreach (var job in definedCommandJobs) @@ -96,17 +100,33 @@ public WorkflowModel Resolve(WorkflowDefinition definition) } } + // Check implicit dependencies and warn if they aren't actually listed as dependencies + foreach (var job in definedCommandJobs) + foreach (var implicitDependencyOption in job.TargetStep.Options.OfType()) + foreach (var implicitDependency in implicitDependencyOption.TargetNames) + if (job.JobDependencies.All(x => x != implicitDependency)) + logger.LogWarning( + "Job '{JobName}' has an implicit dependency on target '{TargetName}' via option '{OptionType}' that is not listed as a dependency. This may lead to unexpected behavior when running the workflow.", + job.Name, + implicitDependency, + implicitDependencyOption.GetType() + .Name); + // Add all targets that are not already defined as jobs var jobs = definedCommandJobs .Concat(buildModel .Targets - .Select(target => target.Name) - .Where(targetName => definedCommandJobs.All(job => job.Name != targetName)) - .Select(targetName => new WorkflowJobModel(targetName, [new(targetName)]) + .Where(target => definedCommandJobs.All(job => job.Name != target.Name)) + .Select(target => new WorkflowJobModel { + Name = target.Name, + TargetStep = new() + { + Name = target.Name, + MatrixDimensions = [], + Options = workflowOptions, + }, JobDependencies = [], - Options = [], - MatrixDimensions = [], })) .ToList(); @@ -134,9 +154,9 @@ public WorkflowModel Resolve(WorkflowDefinition definition) } // Order jobs based on dependencies - - return new(definition.Name) + return new() { + Name = definition.Name, Triggers = definition.Triggers, Options = workflowOptions, Jobs = OrderJobs(jobs), diff --git a/DecSm.Atom/Workflows/Writer/IWorkflowWriter.cs b/src/Invex.Atom.Workflows/Writer/IWorkflowWriter.cs similarity index 91% rename from DecSm.Atom/Workflows/Writer/IWorkflowWriter.cs rename to src/Invex.Atom.Workflows/Writer/IWorkflowWriter.cs index ca1fb40e..5580b8b8 100644 --- a/DecSm.Atom/Workflows/Writer/IWorkflowWriter.cs +++ b/src/Invex.Atom.Workflows/Writer/IWorkflowWriter.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Writer; +namespace Invex.Atom.Workflows.Writer; /// /// Defines a contract for writers that can generate and validate platform-specific workflow files. @@ -31,7 +31,7 @@ public interface IWorkflowWriter /// /// A task that resolves to true if the workflow file is missing or outdated; otherwise, false. /// - Task CheckForDirtyWorkflow(WorkflowModel workflow, CancellationToken cancellationToken = default); + Task CheckForOutdatedWorkflow(WorkflowModel workflow, CancellationToken cancellationToken = default); } /// @@ -53,7 +53,7 @@ public interface IWorkflowWriter : IWorkflowWriter abstract Task IWorkflowWriter.Generate(WorkflowModel workflow, CancellationToken cancellationToken); /// - abstract Task IWorkflowWriter.CheckForDirtyWorkflow( + abstract Task IWorkflowWriter.CheckForOutdatedWorkflow( WorkflowModel workflow, CancellationToken cancellationToken); } diff --git a/DecSm.Atom/Workflows/Writer/WorkflowFileWriter.cs b/src/Invex.Atom.Workflows/Writer/WorkflowFileWriter.cs similarity index 65% rename from DecSm.Atom/Workflows/Writer/WorkflowFileWriter.cs rename to src/Invex.Atom.Workflows/Writer/WorkflowFileWriter.cs index a40a97d9..cd455509 100644 --- a/DecSm.Atom/Workflows/Writer/WorkflowFileWriter.cs +++ b/src/Invex.Atom.Workflows/Writer/WorkflowFileWriter.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Workflows.Writer; +namespace Invex.Atom.Workflows.Writer; /// /// An abstract base class for generating platform-specific workflow files (e.g., for GitHub Actions or Azure DevOps). @@ -12,17 +12,10 @@ /// file format and to specify the file extension. /// [PublicAPI] -public abstract class WorkflowFileWriter(IAtomFileSystem fileSystem, ILogger> logger) +public abstract class WorkflowFileWriter(IRootedFileSystem fileSystem, ILogger> logger) : IWorkflowWriter where T : IWorkflowType { - protected StringBuilder StringBuilder { get; } = new(); - - /// - /// Gets the current indentation level for formatting nested content. - /// - protected int IndentLevel { get; private set; } - /// /// Gets the number of spaces to use for each indentation level. Defaults to 2. /// @@ -47,10 +40,8 @@ public async Task Generate(WorkflowModel workflow, CancellationToken cancellatio { var filePath = FileLocation / $"{workflow.Name}.{FileExtension}"; - WriteWorkflow(workflow); - - var newText = StringBuilder.ToString(); - StringBuilder.Clear(); + var newText = WriteWorkflow(workflow) + .ReplaceLineEndings(); var existingText = fileSystem.File.Exists(filePath) ? await fileSystem.File.ReadAllTextAsync(filePath, cancellationToken) @@ -83,18 +74,15 @@ public async Task Generate(WorkflowModel workflow, CancellationToken cancellatio /// The workflow model to compare against the existing file. /// A cancellation token. /// true if the workflow file is missing or outdated; otherwise, false. - public async Task CheckForDirtyWorkflow(WorkflowModel workflow, CancellationToken cancellationToken = default) + public async Task CheckForOutdatedWorkflow( + WorkflowModel workflow, + CancellationToken cancellationToken = default) { var filePath = FileLocation / $"{workflow.Name}.{FileExtension}"; - WriteWorkflow(workflow); - - var newText = StringBuilder - .ToString() + var newText = WriteWorkflow(workflow) .ReplaceLineEndings(); - StringBuilder.Clear(); - var existingText = fileSystem.File.Exists(filePath) ? await fileSystem.File.ReadAllTextAsync(filePath, cancellationToken) : string.Empty; @@ -105,7 +93,7 @@ public async Task CheckForDirtyWorkflow(WorkflowModel workflow, Cancellati return false; logger.LogInformation( - "Workflow file is dirty and needs to be regenerated: {FilePath}\nExisting:\n{Existing}\nNew:\n{New}", + "Workflow file is outdated and needs to be regenerated: {FilePath}\nExisting:\n{Existing}\nNew:\n{New}", filePath, existingText, newText); @@ -113,42 +101,9 @@ public async Task CheckForDirtyWorkflow(WorkflowModel workflow, Cancellati return true; } - /// - /// Writes a line of text to the output with the current indentation. - /// - /// The text to write. If null, an empty line is written. - protected void WriteLine(string? value = null) - { - if (IndentLevel > 0) - StringBuilder.Append(new string(' ', IndentLevel)); - - StringBuilder.AppendLine(value); - } - - /// - /// Writes a section header and returns a disposable scope that manages indentation for the section's content. - /// - /// The header text for the section. - /// A disposable object that decreases the indentation level upon disposal. - /// - /// - /// using (WriteSection("jobs:")) - /// { - /// WriteLine("build:"); - /// } - /// - /// - protected IDisposable WriteSection(string header) - { - WriteLine(header); - IndentLevel += TabSize; - - return new ActionScope(() => IndentLevel -= TabSize); - } - /// /// When overridden in a derived class, writes the content of the workflow file using the provided helper methods. /// /// The workflow model to write. - protected abstract void WriteWorkflow(WorkflowModel workflow); + protected abstract string WriteWorkflow(WorkflowModel workflow); } diff --git a/src/Invex.Atom.Workflows/_usings.cs b/src/Invex.Atom.Workflows/_usings.cs new file mode 100644 index 00000000..e8863d9d --- /dev/null +++ b/src/Invex.Atom.Workflows/_usings.cs @@ -0,0 +1,33 @@ +global using System.Diagnostics.CodeAnalysis; +global using System.Runtime.CompilerServices; +global using System.Text.Json; +global using System.Text.RegularExpressions; +global using Invex.Atom.Build; +global using Invex.Atom.Build.Args; +global using Invex.Atom.Build.Artifacts; +global using Invex.Atom.Build.BuildOptions; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.Model; +global using Invex.Atom.Build.Exceptions; +global using Invex.Atom.Build.FileSystem; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Build.Params; +global using Invex.Atom.Build.Secrets; +global using Invex.Atom.Build.Util; +global using Invex.Atom.Workflows.Definition; +global using Invex.Atom.Workflows.Definition.Triggers; +global using Invex.Atom.Workflows.Dotnet.Nuget; +global using Invex.Atom.Workflows.Model; +global using Invex.Atom.Workflows.Options; +global using Invex.Atom.Workflows.Options.Injections; +global using Invex.Atom.Workflows.WorkflowContext; +global using Invex.Atom.Workflows.Writer; +global using JetBrains.Annotations; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; + +[assembly: InternalsVisibleTo("Invex.Atom.Build.Tests")] +[assembly: InternalsVisibleTo("Invex.Atom.Workflows.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +[assembly: SuppressMessage("ReSharper", "LocalizableElement")] diff --git a/DecSm.Atom.Analyzers.Tests/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzerTests.cs b/tests/Invex.Atom.Build.Analyzers.Tests/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzerTests.cs similarity index 83% rename from DecSm.Atom.Analyzers.Tests/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzerTests.cs rename to tests/Invex.Atom.Build.Analyzers.Tests/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzerTests.cs index 89dfa95c..c26f6e9c 100644 --- a/DecSm.Atom.Analyzers.Tests/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzerTests.cs +++ b/tests/Invex.Atom.Build.Analyzers.Tests/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzerTests.cs @@ -1,10 +1,9 @@ -using Verifier = DecSm.Atom.Analyzers.Tests.ExtendedAnalyzerVerifier< - DecSm.Atom.Analyzers.AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzer>; +using Verifier = Invex.Atom.Build.Analyzers.Tests.ExtendedAnalyzerVerifier; -namespace DecSm.Atom.Analyzers.Tests; +namespace Invex.Atom.Build.Analyzers.Tests; // ReSharper disable once InconsistentNaming -public class AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzerTests +public sealed class AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzerTests { private void Configure( CSharpAnalyzerTest @@ -29,10 +28,10 @@ private void Configure( // TODO: Use standard .NET 10.0 reference assemblies when available // configuration.ReferenceAssemblies = ReferenceAssemblies.Net.Net100; configuration.ReferenceAssemblies = new("net10.0", - new("Microsoft.NETCore.App.Ref", "10.0.0-rc.1.25451.107"), + new("Microsoft.NETCore.App.Ref", "10.0.8"), Path.Combine("ref", "net10.0")); - var assemblyReference = MetadataReference.CreateFromFile(typeof(MinimalBuildDefinition).Assembly.Location); + var assemblyReference = MetadataReference.CreateFromFile(typeof(BuildDefinition).Assembly.Location); configuration.TestState.AdditionalReferences.AddRange([assemblyReference]); } @@ -40,9 +39,9 @@ private void Configure( public async Task TargetWithRequiresParamOfDirectParam_AlertDiagnostic() { const string text = """ - using DecSm.Atom.Build; - using DecSm.Atom.Build.Definition; - using DecSm.Atom.Params; + using Invex.Atom.Build; + using Invex.Atom.Build.Definition; + using Invex.Atom.Build.Params; public interface IMyTarget : IBuildAccessor { diff --git a/DecSm.Atom.Analyzers.Tests/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProviderTests.cs b/tests/Invex.Atom.Build.Analyzers.Tests/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProviderTests.cs similarity index 75% rename from DecSm.Atom.Analyzers.Tests/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProviderTests.cs rename to tests/Invex.Atom.Build.Analyzers.Tests/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProviderTests.cs index 2a4dad4c..a094fa92 100644 --- a/DecSm.Atom.Analyzers.Tests/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProviderTests.cs +++ b/tests/Invex.Atom.Build.Analyzers.Tests/AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProviderTests.cs @@ -1,10 +1,10 @@ -using CodeFixVerifier = DecSm.Atom.Analyzers.Tests.ExtendedCodeFixVerifier< - DecSm.Atom.Analyzers.AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzer, DecSm.Atom.Analyzers.AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProvider>; +using CodeFixVerifier = Invex.Atom.Build.Analyzers.Tests.ExtendedCodeFixVerifier< + Invex.Atom.Build.Analyzers.AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamAnalyzer, Invex.Atom.Build.Analyzers.AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProvider>; -namespace DecSm.Atom.Analyzers.Tests; +namespace Invex.Atom.Build.Analyzers.Tests; // ReSharper disable once InconsistentNaming -public class AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProviderTests +public sealed class AT0001_TargetRequiringParamShouldNotDirectlyReferenceParamCodeFixProviderTests { private void Configure( CSharpCodeFixTest; + +namespace Invex.Atom.Build.Analyzers.Tests; + +// ReSharper disable once InconsistentNaming +public sealed class AT0002_ConfigureHostPartialMethodNotImplementedAnalyzerTests +{ + private void Configure( + CSharpAnalyzerTest configuration) + { + configuration.SolutionTransforms.Add((solution, projectId) => + { + var project = solution.GetProject(projectId); + + if (project == null) + return solution; + + var parseOptions = (CSharpParseOptions)project.ParseOptions!; + var updatedParseOptions = parseOptions.WithLanguageVersion(LanguageVersion.CSharp14); + + return solution.WithProjectParseOptions(projectId, updatedParseOptions); + }); + + configuration.ReferenceAssemblies = new("net10.0", + new("Microsoft.NETCore.App.Ref", "10.0.8"), + Path.Combine("ref", "net10.0")); + + var assemblyReference = MetadataReference.CreateFromFile(typeof(BuildDefinition).Assembly.Location); + configuration.TestState.AdditionalReferences.AddRange([assemblyReference]); + } + + [Fact] + public async Task InterfaceWithConfigureHost_MissingPartialMethod_AlertDiagnostic() + { + const string text = """ + using Invex.Atom.Build.Hosting; + + [ConfigureHost] + public partial interface IMyHostConfigurator + { + } + """; + + DiagnosticResult[] expected = + [ + Verifier + .Diagnostic() + .WithSpan(4, 26, 4, 45) + .WithArguments("IMyHostConfigurator", "ConfigureHostFromIMyHostConfigurator"), + ]; + + await Verifier.VerifyAnalyzerAsync(text, Configure, expected); + } + + [Fact] + public async Task InterfaceWithConfigureHost_HasPartialMethodImplementation_NoDiagnostic() + { + const string text = """ + using Invex.Atom.Build.Hosting; + using Microsoft.Extensions.Hosting; + + [ConfigureHost] + public partial interface IMyHostConfigurator + { + protected static partial void ConfigureHostFromIMyHostConfigurator(IHost host); + } + + public partial interface IMyHostConfigurator + { + protected static partial void ConfigureHostFromIMyHostConfigurator(IHost host) + { + } + } + """; + + await Verifier.VerifyAnalyzerAsync(text, Configure); + } + + [Fact] + public async Task InterfaceWithoutConfigureHost_NoDiagnostic() + { + const string text = """ + public partial interface INotConfigured + { + } + """; + + await Verifier.VerifyAnalyzerAsync(text, Configure); + } +} diff --git a/tests/Invex.Atom.Build.Analyzers.Tests/AT0002_ConfigureHostPartialMethodNotImplementedCodeFixProviderTests.cs b/tests/Invex.Atom.Build.Analyzers.Tests/AT0002_ConfigureHostPartialMethodNotImplementedCodeFixProviderTests.cs new file mode 100644 index 00000000..38dedcec --- /dev/null +++ b/tests/Invex.Atom.Build.Analyzers.Tests/AT0002_ConfigureHostPartialMethodNotImplementedCodeFixProviderTests.cs @@ -0,0 +1,97 @@ +using Verifier = Invex.Atom.Build.Analyzers.Tests.ExtendedCodeFixVerifier< + Invex.Atom.Build.Analyzers.AT0002_ConfigureHostPartialMethodNotImplementedAnalyzer, Invex.Atom.Build.Analyzers.AT0002_ConfigureHostPartialMethodNotImplementedCodeFixProvider>; + +namespace Invex.Atom.Build.Analyzers.Tests; + +// ReSharper disable once InconsistentNaming +public sealed class AT0002_ConfigureHostPartialMethodNotImplementedCodeFixProviderTests +{ + private void Configure( + CSharpCodeFixTest configuration) + { + configuration.SolutionTransforms.Add((solution, projectId) => + { + var project = solution.GetProject(projectId); + + if (project == null) + return solution; + + var parseOptions = (CSharpParseOptions)project.ParseOptions!; + var updatedParseOptions = parseOptions.WithLanguageVersion(LanguageVersion.CSharp14); + + return solution.WithProjectParseOptions(projectId, updatedParseOptions); + }); + + configuration.ReferenceAssemblies = new("net10.0", + new("Microsoft.NETCore.App.Ref", "10.0.8"), + Path.Combine("ref", "net10.0")); + + var assemblyReference = MetadataReference.CreateFromFile(typeof(BuildDefinition).Assembly.Location); + configuration.TestState.AdditionalReferences.AddRange([assemblyReference]); + } + + [Fact] + public async Task CodeFix_AddsPartialMethodImplementation() + { + const string text = """ + using Invex.Atom.Build.Hosting; + + [ConfigureHost] + public partial interface IMyHostConfigurator + { + } + """; + + const string fixedText = """ + using Invex.Atom.Build.Hosting; + using Microsoft.Extensions.Hosting; + + [ConfigureHost] + public partial interface IMyHostConfigurator + { + protected static partial void ConfigureHostFromIMyHostConfigurator(IHost host) + { + } + } + """; + + var expected = Verifier + .Diagnostic() + .WithSpan(4, 26, 4, 45) + .WithArguments("IMyHostConfigurator", "ConfigureHostFromIMyHostConfigurator"); + + await Verifier.VerifyCodeFixAsync(text, expected, fixedText, Configure); + } + + [Fact] + public async Task CodeFix_AddsPartialMethodImplementationToEmptyType() + { + const string text = """ + using Invex.Atom.Build.Hosting; + + [ConfigureHost] + public partial interface IMyHostConfigurator; + """; + + const string fixedText = """ + using Invex.Atom.Build.Hosting; + using Microsoft.Extensions.Hosting; + + [ConfigureHost] + public partial interface IMyHostConfigurator + { + protected static partial void ConfigureHostFromIMyHostConfigurator(IHost host) + { + } + } + """; + + var expected = Verifier + .Diagnostic() + .WithSpan(4, 26, 4, 45) + .WithArguments("IMyHostConfigurator", "ConfigureHostFromIMyHostConfigurator"); + + await Verifier.VerifyCodeFixAsync(text, expected, fixedText, Configure); + } +} diff --git a/tests/Invex.Atom.Build.Analyzers.Tests/AT0003_ConfigureHostBuilderPartialMethodNotImplementedAnalyzerTests.cs b/tests/Invex.Atom.Build.Analyzers.Tests/AT0003_ConfigureHostBuilderPartialMethodNotImplementedAnalyzerTests.cs new file mode 100644 index 00000000..429c3e2d --- /dev/null +++ b/tests/Invex.Atom.Build.Analyzers.Tests/AT0003_ConfigureHostBuilderPartialMethodNotImplementedAnalyzerTests.cs @@ -0,0 +1,92 @@ +using Verifier = Invex.Atom.Build.Analyzers.Tests.ExtendedAnalyzerVerifier< + Invex.Atom.Build.Analyzers.AT0003_ConfigureHostBuilderPartialMethodNotImplementedAnalyzer>; + +namespace Invex.Atom.Build.Analyzers.Tests; + +// ReSharper disable once InconsistentNaming +public sealed class AT0003_ConfigureHostBuilderPartialMethodNotImplementedAnalyzerTests +{ + private void Configure( + CSharpAnalyzerTest + configuration) + { + configuration.SolutionTransforms.Add((solution, projectId) => + { + var project = solution.GetProject(projectId); + + if (project == null) + return solution; + + var parseOptions = (CSharpParseOptions)project.ParseOptions!; + var updatedParseOptions = parseOptions.WithLanguageVersion(LanguageVersion.CSharp14); + + return solution.WithProjectParseOptions(projectId, updatedParseOptions); + }); + + configuration.ReferenceAssemblies = new("net10.0", + new("Microsoft.NETCore.App.Ref", "10.0.8"), + Path.Combine("ref", "net10.0")); + + var assemblyReference = MetadataReference.CreateFromFile(typeof(BuildDefinition).Assembly.Location); + configuration.TestState.AdditionalReferences.AddRange([assemblyReference]); + } + + [Fact] + public async Task InterfaceWithConfigureHostBuilder_MissingPartialMethod_AlertDiagnostic() + { + const string text = """ + using Invex.Atom.Build.Hosting; + + [ConfigureHostBuilder] + public partial interface IMyBuilderConfigurator + { + } + """; + + DiagnosticResult[] expected = + [ + Verifier + .Diagnostic() + .WithSpan(4, 26, 4, 48) + .WithArguments("IMyBuilderConfigurator", "ConfigureBuilderFromIMyBuilderConfigurator"), + ]; + + await Verifier.VerifyAnalyzerAsync(text, Configure, expected); + } + + [Fact] + public async Task InterfaceWithConfigureHostBuilder_HasPartialMethodImplementation_NoDiagnostic() + { + const string text = """ + using Invex.Atom.Build.Hosting; + using Microsoft.Extensions.Hosting; + + [ConfigureHostBuilder] + public partial interface IMyBuilderConfigurator + { + protected static partial void ConfigureBuilderFromIMyBuilderConfigurator(IHostApplicationBuilder builder); + } + + public partial interface IMyBuilderConfigurator + { + protected static partial void ConfigureBuilderFromIMyBuilderConfigurator(IHostApplicationBuilder builder) + { + } + } + """; + + await Verifier.VerifyAnalyzerAsync(text, Configure); + } + + [Fact] + public async Task InterfaceWithoutConfigureHostBuilder_NoDiagnostic() + { + const string text = """ + public partial interface INotConfigured + { + } + """; + + await Verifier.VerifyAnalyzerAsync(text, Configure); + } +} diff --git a/tests/Invex.Atom.Build.Analyzers.Tests/AT0003_ConfigureHostBuilderPartialMethodNotImplementedCodeFixProviderTests.cs b/tests/Invex.Atom.Build.Analyzers.Tests/AT0003_ConfigureHostBuilderPartialMethodNotImplementedCodeFixProviderTests.cs new file mode 100644 index 00000000..03952ee3 --- /dev/null +++ b/tests/Invex.Atom.Build.Analyzers.Tests/AT0003_ConfigureHostBuilderPartialMethodNotImplementedCodeFixProviderTests.cs @@ -0,0 +1,97 @@ +using Verifier = Invex.Atom.Build.Analyzers.Tests.ExtendedCodeFixVerifier< + Invex.Atom.Build.Analyzers.AT0003_ConfigureHostBuilderPartialMethodNotImplementedAnalyzer, Invex.Atom.Build.Analyzers.AT0003_ConfigureHostBuilderPartialMethodNotImplementedCodeFixProvider>; + +namespace Invex.Atom.Build.Analyzers.Tests; + +// ReSharper disable once InconsistentNaming +public sealed class AT0003_ConfigureHostBuilderPartialMethodNotImplementedCodeFixProviderTests +{ + private void Configure( + CSharpCodeFixTest configuration) + { + configuration.SolutionTransforms.Add((solution, projectId) => + { + var project = solution.GetProject(projectId); + + if (project == null) + return solution; + + var parseOptions = (CSharpParseOptions)project.ParseOptions!; + var updatedParseOptions = parseOptions.WithLanguageVersion(LanguageVersion.CSharp14); + + return solution.WithProjectParseOptions(projectId, updatedParseOptions); + }); + + configuration.ReferenceAssemblies = new("net10.0", + new("Microsoft.NETCore.App.Ref", "10.0.8"), + Path.Combine("ref", "net10.0")); + + var assemblyReference = MetadataReference.CreateFromFile(typeof(BuildDefinition).Assembly.Location); + configuration.TestState.AdditionalReferences.AddRange([assemblyReference]); + } + + [Fact] + public async Task CodeFix_AddsPartialMethodImplementation() + { + const string text = """ + using Invex.Atom.Build.Hosting; + + [ConfigureHostBuilder] + public partial interface IMyBuilderConfigurator + { + } + """; + + const string fixedText = """ + using Invex.Atom.Build.Hosting; + using Microsoft.Extensions.Hosting; + + [ConfigureHostBuilder] + public partial interface IMyBuilderConfigurator + { + protected static partial void ConfigureBuilderFromIMyBuilderConfigurator(IHostApplicationBuilder builder) + { + } + } + """; + + var expected = Verifier + .Diagnostic() + .WithSpan(4, 26, 4, 48) + .WithArguments("IMyBuilderConfigurator", "ConfigureBuilderFromIMyBuilderConfigurator"); + + await Verifier.VerifyCodeFixAsync(text, expected, fixedText, Configure); + } + + [Fact] + public async Task CodeFix_AddsPartialMethodImplementationToEmptyType() + { + const string text = """ + using Invex.Atom.Build.Hosting; + + [ConfigureHostBuilder] + public partial interface IMyBuilderConfigurator; + """; + + const string fixedText = """ + using Invex.Atom.Build.Hosting; + using Microsoft.Extensions.Hosting; + + [ConfigureHostBuilder] + public partial interface IMyBuilderConfigurator + { + protected static partial void ConfigureBuilderFromIMyBuilderConfigurator(IHostApplicationBuilder builder) + { + } + } + """; + + var expected = Verifier + .Diagnostic() + .WithSpan(4, 26, 4, 48) + .WithArguments("IMyBuilderConfigurator", "ConfigureBuilderFromIMyBuilderConfigurator"); + + await Verifier.VerifyCodeFixAsync(text, expected, fixedText, Configure); + } +} diff --git a/DecSm.Atom.Analyzers.Tests/ExtendedAnalyzerVerifier.cs b/tests/Invex.Atom.Build.Analyzers.Tests/ExtendedAnalyzerVerifier.cs similarity index 95% rename from DecSm.Atom.Analyzers.Tests/ExtendedAnalyzerVerifier.cs rename to tests/Invex.Atom.Build.Analyzers.Tests/ExtendedAnalyzerVerifier.cs index d0a9a0da..00a4445d 100644 --- a/DecSm.Atom.Analyzers.Tests/ExtendedAnalyzerVerifier.cs +++ b/tests/Invex.Atom.Build.Analyzers.Tests/ExtendedAnalyzerVerifier.cs @@ -1,7 +1,4 @@ -using JetBrains.Annotations; -using Microsoft.CodeAnalysis.Diagnostics; - -namespace DecSm.Atom.Analyzers.Tests; +namespace Invex.Atom.Build.Analyzers.Tests; [PublicAPI] public sealed class ExtendedAnalyzerVerifier : ExtendedAnalyzerVerifier : ExtendedCodeFixVerifier : ExtendedCodeFixVerifier, DefaultVerifier> where TAnalyzer : DiagnosticAnalyzer, new() where TCodeFix : CodeFixProvider, new(); diff --git a/DecSm.Atom.Analyzers.Tests/DecSm.Atom.Analyzers.Tests.csproj b/tests/Invex.Atom.Build.Analyzers.Tests/Invex.Atom.Build.Analyzers.Tests.csproj similarity index 73% rename from DecSm.Atom.Analyzers.Tests/DecSm.Atom.Analyzers.Tests.csproj rename to tests/Invex.Atom.Build.Analyzers.Tests/Invex.Atom.Build.Analyzers.Tests.csproj index 398d3023..4c81f138 100644 --- a/DecSm.Atom.Analyzers.Tests/DecSm.Atom.Analyzers.Tests.csproj +++ b/tests/Invex.Atom.Build.Analyzers.Tests/Invex.Atom.Build.Analyzers.Tests.csproj @@ -8,7 +8,7 @@ - + @@ -19,9 +19,8 @@ - - - + + diff --git a/DecSm.Atom.Analyzers.Tests/_usings.cs b/tests/Invex.Atom.Build.Analyzers.Tests/_usings.cs similarity index 52% rename from DecSm.Atom.Analyzers.Tests/_usings.cs rename to tests/Invex.Atom.Build.Analyzers.Tests/_usings.cs index 5a62d0e0..0f9ae437 100644 --- a/DecSm.Atom.Analyzers.Tests/_usings.cs +++ b/tests/Invex.Atom.Build.Analyzers.Tests/_usings.cs @@ -1,6 +1,9 @@ -global using DecSm.Atom.Build.Definition; +global using Invex.Atom.Build.Definition; +global using JetBrains.Annotations; global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.CodeFixes; global using Microsoft.CodeAnalysis.CSharp; global using Microsoft.CodeAnalysis.CSharp.Testing; +global using Microsoft.CodeAnalysis.Diagnostics; global using Microsoft.CodeAnalysis.Testing; global using Xunit; diff --git a/DecSm.Atom.SourceGenerators.Tests/DecSm.Atom.SourceGenerators.Tests.csproj b/tests/Invex.Atom.Build.SourceGenerators.Tests/Invex.Atom.Build.SourceGenerators.Tests.csproj similarity index 90% rename from DecSm.Atom.SourceGenerators.Tests/DecSm.Atom.SourceGenerators.Tests.csproj rename to tests/Invex.Atom.Build.SourceGenerators.Tests/Invex.Atom.Build.SourceGenerators.Tests.csproj index 4c78d706..484e2f37 100644 --- a/DecSm.Atom.SourceGenerators.Tests/DecSm.Atom.SourceGenerators.Tests.csproj +++ b/tests/Invex.Atom.Build.SourceGenerators.Tests/Invex.Atom.Build.SourceGenerators.Tests.csproj @@ -3,17 +3,12 @@ net10.0;net9.0;net8.0 false - false - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + @@ -23,6 +18,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -42,8 +41,8 @@ - - + + diff --git a/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.BuildDefinition_GeneratesSource.verified.txt b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.BuildDefinition_GeneratesSource.verified.txt new file mode 100644 index 00000000..3329e627 --- /dev/null +++ b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.BuildDefinition_GeneratesSource.verified.txt @@ -0,0 +1,79 @@ +// + +#nullable enable +#pragma warning disable CS0169 + +global using static TestNamespace.DefaultTestDefinition; + +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Invex.Atom.Build.Definition; +using Invex.Atom.Build.Params; +using Invex.FileSystem; +using Invex.Process; + +namespace TestNamespace; + +[JetBrains.Annotations.PublicAPI] +partial class DefaultTestDefinition : Invex.Atom.Build.Definition.IBuildDefinition, Invex.Atom.Build.Hosting.IConfigureHost +{ + public DefaultTestDefinition(System.IServiceProvider services) : base(services) { } + + private ILogger Logger => Services.GetRequiredService().CreateLogger("TestNamespace.DefaultTestDefinition"); + + private IRootedFileSystem FileSystem => GetService(); + + private IProcessRunner ProcessRunner => GetService(); + + private T GetService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() + where T : notnull => + typeof(T).GetInterface(nameof(IBuildDefinition)) != null + ? (T)(IBuildDefinition)this + : Services.GetRequiredService(); + + private IEnumerable GetServices<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() + where T : notnull => + typeof(T).GetInterface(nameof(IBuildDefinition)) != null + ? [(T)(IBuildDefinition)this] + : Services.GetServices(); + + [return: NotNullIfNotNull(nameof(defaultValue))] + private T? GetParam<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + Expression> parameterExpression, + T? defaultValue = default, + Func? converter = null) => + Services + .GetRequiredService() + .GetParam(parameterExpression, defaultValue, converter); + + #region Targets + + public override System.Collections.Generic.IReadOnlyDictionary TargetDefinitions { get; } = new System.Collections.Generic.Dictionary(); + + #endregion Targets + + #region Params + + public override System.Collections.Generic.IReadOnlyDictionary ParamDefinitions { get; } = new System.Collections.Generic.Dictionary(); + + public sealed class ParamsData() { } + + private ParamsData? _params; + + public override object? AccessParam(string paramName) => throw new System.ArgumentException($"Param with name '{paramName}' is not defined in the build definition.", nameof(paramName)); + + #endregion Params + + #region Host + + public void ConfigureBuildHost(Microsoft.Extensions.Hosting.IHost builder) { } + + public void ConfigureBuildHostBuilder(Microsoft.Extensions.Hosting.IHostApplicationBuilder builder) + { + Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(builder.Services, static p => (Invex.Atom.Build.Definition.IBuildDefinition)p.GetRequiredService()); + } + + #endregion Host +} diff --git a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalBuildDefinition_WithBaseType_GeneratesSource.verified.txt b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.DefinitionWithChainedParam_GeneratesSource.verified.txt similarity index 63% rename from DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalBuildDefinition_WithBaseType_GeneratesSource.verified.txt rename to tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.DefinitionWithChainedParam_GeneratesSource.verified.txt index a0b650ea..d71f40c9 100644 --- a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalBuildDefinition_WithBaseType_GeneratesSource.verified.txt +++ b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.DefinitionWithChainedParam_GeneratesSource.verified.txt @@ -1,28 +1,29 @@ // #nullable enable +#pragma warning disable CS0169 -global using static TestNamespace.MinimalTestDefinition; +global using static TestNamespace.ChainedParamBuild; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using DecSm.Atom.Build.Definition; -using DecSm.Atom.Params; -using DecSm.Atom.Paths; -using DecSm.Atom.Process; +using Invex.Atom.Build.Definition; +using Invex.Atom.Build.Params; +using Invex.FileSystem; +using Invex.Process; namespace TestNamespace; [JetBrains.Annotations.PublicAPI] -partial class MinimalTestDefinition : DecSm.Atom.Build.Definition.IBuildDefinition +partial class ChainedParamBuild : Invex.Atom.Build.Definition.IBuildDefinition { - public MinimalTestDefinition(System.IServiceProvider services) : base(services) { } + public ChainedParamBuild(System.IServiceProvider services) : base(services) { } - private ILogger Logger => Services.GetRequiredService().CreateLogger("TestNamespace.MinimalTestDefinition"); + private ILogger Logger => Services.GetRequiredService().CreateLogger("TestNamespace.ChainedParamBuild"); - private IAtomFileSystem FileSystem => GetService(); + private IRootedFileSystem FileSystem => GetService(); private IProcessRunner ProcessRunner => GetService(); @@ -49,22 +50,23 @@ partial class MinimalTestDefinition : DecSm.Atom.Build.Definition.IBuildDefiniti #region Targets - public override System.Collections.Generic.IReadOnlyDictionary TargetDefinitions { get; } = new System.Collections.Generic.Dictionary(); + private System.Collections.Generic.IReadOnlyDictionary? _targetDefinitions; -private static DecSm.Atom.Workflows.Definition.WorkflowTargetDefinition Target(string name) => throw new System.ArgumentException($"Target with name '{name}' is not defined in the build definition.", nameof(name)); + public override System.Collections.Generic.IReadOnlyDictionary TargetDefinitions => _targetDefinitions ??= new System.Collections.Generic.Dictionary + { + { nameof(TestNamespace.IChainedParamTarget.ChainedParamTarget), ((TestNamespace.IChainedParamTarget)this).ChainedParamTarget }, + }; #endregion Targets #region Params - public override System.Collections.Generic.IReadOnlyDictionary ParamDefinitions { get; } = new System.Collections.Generic.Dictionary(); + public override System.Collections.Generic.IReadOnlyDictionary ParamDefinitions { get; } = new System.Collections.Generic.Dictionary(); public sealed class ParamsData() { } private ParamsData? _params; - public ParamsData Params => _params ??= new(); - public override object? AccessParam(string paramName) => throw new System.ArgumentException($"Param with name '{paramName}' is not defined in the build definition.", nameof(paramName)); #endregion Params diff --git a/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalBuildDefinition_WithBaseType_GeneratesSource.verified.txt b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalBuildDefinition_WithBaseType_GeneratesSource.verified.txt new file mode 100644 index 00000000..491be238 --- /dev/null +++ b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalBuildDefinition_WithBaseType_GeneratesSource.verified.txt @@ -0,0 +1,79 @@ +// + +#nullable enable +#pragma warning disable CS0169 + +global using static TestNamespace.MinimalTestDefinition; + +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Invex.Atom.Build.Definition; +using Invex.Atom.Build.Params; +using Invex.FileSystem; +using Invex.Process; + +namespace TestNamespace; + +[JetBrains.Annotations.PublicAPI] +partial class MinimalTestDefinition : Invex.Atom.Build.Definition.IBuildDefinition, Invex.Atom.Build.Hosting.IConfigureHost +{ + public MinimalTestDefinition(System.IServiceProvider services) : base(services) { } + + private ILogger Logger => Services.GetRequiredService().CreateLogger("TestNamespace.MinimalTestDefinition"); + + private IRootedFileSystem FileSystem => GetService(); + + private IProcessRunner ProcessRunner => GetService(); + + private T GetService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() + where T : notnull => + typeof(T).GetInterface(nameof(IBuildDefinition)) != null + ? (T)(IBuildDefinition)this + : Services.GetRequiredService(); + + private IEnumerable GetServices<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() + where T : notnull => + typeof(T).GetInterface(nameof(IBuildDefinition)) != null + ? [(T)(IBuildDefinition)this] + : Services.GetServices(); + + [return: NotNullIfNotNull(nameof(defaultValue))] + private T? GetParam<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + Expression> parameterExpression, + T? defaultValue = default, + Func? converter = null) => + Services + .GetRequiredService() + .GetParam(parameterExpression, defaultValue, converter); + + #region Targets + + public override System.Collections.Generic.IReadOnlyDictionary TargetDefinitions { get; } = new System.Collections.Generic.Dictionary(); + + #endregion Targets + + #region Params + + public override System.Collections.Generic.IReadOnlyDictionary ParamDefinitions { get; } = new System.Collections.Generic.Dictionary(); + + public sealed class ParamsData() { } + + private ParamsData? _params; + + public override object? AccessParam(string paramName) => throw new System.ArgumentException($"Param with name '{paramName}' is not defined in the build definition.", nameof(paramName)); + + #endregion Params + + #region Host + + public void ConfigureBuildHost(Microsoft.Extensions.Hosting.IHost builder) { } + + public void ConfigureBuildHostBuilder(Microsoft.Extensions.Hosting.IHostApplicationBuilder builder) + { + Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton(builder.Services, static p => (Invex.Atom.Build.Definition.IBuildDefinition)p.GetRequiredService()); + } + + #endregion Host +} diff --git a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalBuildDefinition_WithoutBaseType_GeneratesSource.verified.txt b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalBuildDefinition_WithoutBaseType_GeneratesSource.verified.txt similarity index 71% rename from DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalBuildDefinition_WithoutBaseType_GeneratesSource.verified.txt rename to tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalBuildDefinition_WithoutBaseType_GeneratesSource.verified.txt index a0b650ea..69dd8556 100644 --- a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalBuildDefinition_WithoutBaseType_GeneratesSource.verified.txt +++ b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.MinimalBuildDefinition_WithoutBaseType_GeneratesSource.verified.txt @@ -1,6 +1,7 @@ // #nullable enable +#pragma warning disable CS0169 global using static TestNamespace.MinimalTestDefinition; @@ -8,21 +9,21 @@ using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using DecSm.Atom.Build.Definition; -using DecSm.Atom.Params; -using DecSm.Atom.Paths; -using DecSm.Atom.Process; +using Invex.Atom.Build.Definition; +using Invex.Atom.Build.Params; +using Invex.FileSystem; +using Invex.Process; namespace TestNamespace; [JetBrains.Annotations.PublicAPI] -partial class MinimalTestDefinition : DecSm.Atom.Build.Definition.IBuildDefinition +partial class MinimalTestDefinition : Invex.Atom.Build.Definition.IBuildDefinition { public MinimalTestDefinition(System.IServiceProvider services) : base(services) { } private ILogger Logger => Services.GetRequiredService().CreateLogger("TestNamespace.MinimalTestDefinition"); - private IAtomFileSystem FileSystem => GetService(); + private IRootedFileSystem FileSystem => GetService(); private IProcessRunner ProcessRunner => GetService(); @@ -49,22 +50,18 @@ partial class MinimalTestDefinition : DecSm.Atom.Build.Definition.IBuildDefiniti #region Targets - public override System.Collections.Generic.IReadOnlyDictionary TargetDefinitions { get; } = new System.Collections.Generic.Dictionary(); - -private static DecSm.Atom.Workflows.Definition.WorkflowTargetDefinition Target(string name) => throw new System.ArgumentException($"Target with name '{name}' is not defined in the build definition.", nameof(name)); + public override System.Collections.Generic.IReadOnlyDictionary TargetDefinitions { get; } = new System.Collections.Generic.Dictionary(); #endregion Targets #region Params - public override System.Collections.Generic.IReadOnlyDictionary ParamDefinitions { get; } = new System.Collections.Generic.Dictionary(); + public override System.Collections.Generic.IReadOnlyDictionary ParamDefinitions { get; } = new System.Collections.Generic.Dictionary(); public sealed class ParamsData() { } private ParamsData? _params; - public ParamsData Params => _params ??= new(); - public override object? AccessParam(string paramName) => throw new System.ArgumentException($"Param with name '{paramName}' is not defined in the build definition.", nameof(paramName)); #endregion Params diff --git a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.cs b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.cs similarity index 79% rename from DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.cs rename to tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.cs index 1a6087fb..bf2390cf 100644 --- a/DecSm.Atom.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.cs +++ b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/BuildDefinitionSourceGeneratorTests.cs @@ -1,14 +1,14 @@ -namespace DecSm.Atom.SourceGenerators.Tests.Tests; +namespace Invex.Atom.Build.SourceGenerators.Tests.Tests; [TestFixture] -public class BuildDefinitionSourceGeneratorTests +internal sealed class BuildDefinitionSourceGeneratorTests { [Test] public async Task MinimalBuildDefinition_WithoutBaseType_GeneratesSource() { // Arrange const string source = """ - using DecSm.Atom.Build.Definition; + using Invex.Atom.Build.Definition; namespace TestNamespace; @@ -18,8 +18,7 @@ public partial class MinimalTestDefinition; // Act var generatedText = - TestUtils.GetGeneratedSource(source, - typeof(MinimalBuildDefinition).Assembly); + TestUtils.GetGeneratedSource(source, typeof(BuildDefinition).Assembly); // Assert await Verify(generatedText); @@ -30,18 +29,17 @@ public async Task MinimalBuildDefinition_WithBaseType_GeneratesSource() { // Arrange const string source = """ - using DecSm.Atom.Build.Definition; + using Invex.Atom.Build.Definition; namespace TestNamespace; [BuildDefinition] - public partial class MinimalTestDefinition : MinimalBuildDefinition; + public partial class MinimalTestDefinition : BuildDefinition; """; // Act var generatedText = - TestUtils.GetGeneratedSource(source, - typeof(MinimalBuildDefinition).Assembly); + TestUtils.GetGeneratedSource(source, typeof(BuildDefinition).Assembly); // Assert await Verify(generatedText); @@ -52,7 +50,7 @@ public async Task BuildDefinition_GeneratesSource() { // Arrange const string source = """ - using DecSm.Atom.Build.Definition; + using Invex.Atom.Build.Definition; namespace TestNamespace; @@ -62,8 +60,7 @@ public partial class DefaultTestDefinition : BuildDefinition; // Act var generatedText = - TestUtils.GetGeneratedSource(source, - typeof(MinimalBuildDefinition).Assembly); + TestUtils.GetGeneratedSource(source, typeof(BuildDefinition).Assembly); // Assert await Verify(generatedText); @@ -74,8 +71,8 @@ public async Task DefinitionWithChainedParam_GeneratesSource() { // Arrange const string source = """ - using DecSm.Atom.Build.Definition; - using DecSm.Atom.Params; + using Invex.Atom.Build.Definition; + using Invex.Atom.Params; namespace TestNamespace; @@ -99,8 +96,7 @@ public interface IChainedParamTarget : IBuildAccessor // Act var generatedText = - TestUtils.GetGeneratedSource(source, - typeof(MinimalBuildDefinition).Assembly); + TestUtils.GetGeneratedSource(source, typeof(BuildDefinition).Assembly); // Assert await Verify(generatedText); diff --git a/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/GenerateEntryPointSourceGeneratorTests.EmptyDefinition_GeneratesDefaultSource.verified.txt b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/GenerateEntryPointSourceGeneratorTests.EmptyDefinition_GeneratesDefaultSource.verified.txt new file mode 100644 index 00000000..1415418a --- /dev/null +++ b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/GenerateEntryPointSourceGeneratorTests.EmptyDefinition_GeneratesDefaultSource.verified.txt @@ -0,0 +1,3 @@ +// + +Invex.Atom.Build.Hosting.AtomHost.Run(args); \ No newline at end of file diff --git a/DecSm.Atom.SourceGenerators.Tests/Tests/GenerateEntryPointSourceGeneratorTests.cs b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/GenerateEntryPointSourceGeneratorTests.cs similarity index 65% rename from DecSm.Atom.SourceGenerators.Tests/Tests/GenerateEntryPointSourceGeneratorTests.cs rename to tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/GenerateEntryPointSourceGeneratorTests.cs index 995facdd..2ad5315b 100644 --- a/DecSm.Atom.SourceGenerators.Tests/Tests/GenerateEntryPointSourceGeneratorTests.cs +++ b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/GenerateEntryPointSourceGeneratorTests.cs @@ -1,14 +1,14 @@ -namespace DecSm.Atom.SourceGenerators.Tests.Tests; +namespace Invex.Atom.Build.SourceGenerators.Tests.Tests; -public class GenerateEntryPointSourceGeneratorTests +internal sealed class GenerateEntryPointSourceGeneratorTests { [Test] public async Task EmptyDefinition_GeneratesDefaultSource() { // Arrange const string source = """ - using DecSm.Atom.Build.Definition; - using DecSm.Atom.Hosting; + using Invex.Atom.Build.Definition; + using Invex.Atom.Build.Hosting; namespace TestNamespace; @@ -19,8 +19,7 @@ public partial class TestBuildDefinition : BuildDefinition; // Act var generatedText = - TestUtils.GetGeneratedSource(source, - typeof(MinimalBuildDefinition).Assembly); + TestUtils.GetGeneratedSource(source, typeof(BuildDefinition).Assembly); // Assert await Verify(generatedText); diff --git a/DecSm.Atom.SourceGenerators.Tests/Tests/GenerateInterfaceMembersGeneratorTests.MinimalBuildDefinition_WithGeneratedInterfaceMember_GeneratesSource.verified.txt b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/GenerateInterfaceMembersGeneratorTests.MinimalBuildDefinition_WithGeneratedInterfaceMember_GeneratesSource.verified.txt similarity index 100% rename from DecSm.Atom.SourceGenerators.Tests/Tests/GenerateInterfaceMembersGeneratorTests.MinimalBuildDefinition_WithGeneratedInterfaceMember_GeneratesSource.verified.txt rename to tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/GenerateInterfaceMembersGeneratorTests.MinimalBuildDefinition_WithGeneratedInterfaceMember_GeneratesSource.verified.txt diff --git a/DecSm.Atom.SourceGenerators.Tests/Tests/GenerateInterfaceMembersGeneratorTests.cs b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/GenerateInterfaceMembersGeneratorTests.cs similarity index 90% rename from DecSm.Atom.SourceGenerators.Tests/Tests/GenerateInterfaceMembersGeneratorTests.cs rename to tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/GenerateInterfaceMembersGeneratorTests.cs index 7e5e2ce3..8a336ee4 100644 --- a/DecSm.Atom.SourceGenerators.Tests/Tests/GenerateInterfaceMembersGeneratorTests.cs +++ b/tests/Invex.Atom.Build.SourceGenerators.Tests/Tests/GenerateInterfaceMembersGeneratorTests.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.SourceGenerators.Tests.Tests; +namespace Invex.Atom.Build.SourceGenerators.Tests.Tests; [TestFixture] public sealed class GenerateInterfaceMembersGeneratorTests @@ -8,7 +8,7 @@ public async Task MinimalBuildDefinition_WithGeneratedInterfaceMember_GeneratesS { // Arrange const string source = """ - using DecSm.Atom.Build.Definition; + using Invex.Atom.Build.Definition; using System.Collections.Generic; namespace TestNamespace; @@ -40,7 +40,7 @@ void MethodWithOptionalParameter(int param1 = 1, string? param2 = null, string p // Act var generatedText = TestUtils.GetGeneratedSource(source, - typeof(MinimalBuildDefinition).Assembly); + typeof(BuildDefinition).Assembly); // Assert await Verify(generatedText); diff --git a/DecSm.Atom.SourceGenerators.Tests/Utils/Initializer.cs b/tests/Invex.Atom.Build.SourceGenerators.Tests/Utils/Initializer.cs similarity index 77% rename from DecSm.Atom.SourceGenerators.Tests/Utils/Initializer.cs rename to tests/Invex.Atom.Build.SourceGenerators.Tests/Utils/Initializer.cs index 264a5a4c..cee36123 100644 --- a/DecSm.Atom.SourceGenerators.Tests/Utils/Initializer.cs +++ b/tests/Invex.Atom.Build.SourceGenerators.Tests/Utils/Initializer.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.SourceGenerators.Tests.Utils; +namespace Invex.Atom.Build.SourceGenerators.Tests.Utils; public static class Initializer { diff --git a/DecSm.Atom.SourceGenerators.Tests/Utils/TestUtils.cs b/tests/Invex.Atom.Build.SourceGenerators.Tests/Utils/TestUtils.cs similarity index 95% rename from DecSm.Atom.SourceGenerators.Tests/Utils/TestUtils.cs rename to tests/Invex.Atom.Build.SourceGenerators.Tests/Utils/TestUtils.cs index 2d314107..06acf211 100644 --- a/DecSm.Atom.SourceGenerators.Tests/Utils/TestUtils.cs +++ b/tests/Invex.Atom.Build.SourceGenerators.Tests/Utils/TestUtils.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.SourceGenerators.Tests.Utils; +namespace Invex.Atom.Build.SourceGenerators.Tests.Utils; public static class TestUtils { diff --git a/DecSm.Atom.SourceGenerators.Tests/_usings.cs b/tests/Invex.Atom.Build.SourceGenerators.Tests/_usings.cs similarity index 71% rename from DecSm.Atom.SourceGenerators.Tests/_usings.cs rename to tests/Invex.Atom.Build.SourceGenerators.Tests/_usings.cs index d09a0176..d5377ce9 100644 --- a/DecSm.Atom.SourceGenerators.Tests/_usings.cs +++ b/tests/Invex.Atom.Build.SourceGenerators.Tests/_usings.cs @@ -1,8 +1,8 @@ global using System.Reflection; global using System.Runtime.CompilerServices; global using Basic.Reference.Assemblies; -global using DecSm.Atom.Build.Definition; -global using DecSm.Atom.SourceGenerators.Tests.Utils; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.SourceGenerators.Tests.Utils; global using DiffEngine; global using Microsoft.CodeAnalysis; global using Microsoft.CodeAnalysis.CSharp; diff --git a/tests/Invex.Atom.Build.Tests/ApiSurfaceTests/PublicApiSurfaceTests.VerifyPublicApiSurface.verified.txt b/tests/Invex.Atom.Build.Tests/ApiSurfaceTests/PublicApiSurfaceTests.VerifyPublicApiSurface.verified.txt new file mode 100644 index 00000000..4c1376ee --- /dev/null +++ b/tests/Invex.Atom.Build.Tests/ApiSurfaceTests/PublicApiSurfaceTests.VerifyPublicApiSurface.verified.txt @@ -0,0 +1,1624 @@ +[ + { + Name: Invex.Atom.Build.Args.CommandArg, + Members: [ + { + Name: $ + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: Name + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Args.CommandLineArgs, + Members: [ + { + Name: $ + }, + { + Name: Args + }, + { + Name: Commands + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: GetValidationErrors + }, + { + Name: HasHeadless + }, + { + Name: HasHelp + }, + { + Name: HasInteractive + }, + { + Name: HasProject + }, + { + Name: HasSkip + }, + { + Name: HasVerbose + }, + { + Name: IsValid + }, + { + Name: Params + }, + { + Name: ProjectName + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Args.HeadlessArg, + Members: [ + { + Name: $ + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Args.HelpArg, + Members: [ + { + Name: $ + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Args.IArg + }, + { + Name: Invex.Atom.Build.Args.InteractiveArg, + Members: [ + { + Name: $ + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Args.ParamArg, + Members: [ + { + Name: $ + }, + { + Name: ArgName + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: ParamName + }, + { + Name: ParamValue + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Args.ProjectArg, + Members: [ + { + Name: $ + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: ProjectName + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Args.SkipArg, + Members: [ + { + Name: $ + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Args.VerboseArg, + Members: [ + { + Name: $ + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Artifacts.IArtifactProvider, + Members: [ + { + Name: Cleanup + }, + { + Name: GetStoredRunIdentifiers + }, + { + Name: RequiredParams + }, + { + Name: RetrieveArtifacts + }, + { + Name: StoreArtifacts + } + ] + }, + { + Name: Invex.Atom.Build.Artifacts.IAtomArtifactsParam, + Members: [ + { + Name: AtomArtifacts + } + ] + }, + { + Name: Invex.Atom.Build.Artifacts.IRetrieveArtifact, + Members: [ + { + Name: RetrieveArtifact + } + ] + }, + { + Name: Invex.Atom.Build.Artifacts.IStoreArtifact, + Members: [ + { + Name: StoreArtifact + } + ] + }, + { + Name: Invex.Atom.Build.AtomProjectData, + Members: [ + { + Name: $ + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: IsFileBasedApp + }, + { + Name: ProjectName + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.BuildInfo.IBuildIdProvider, + Members: [ + { + Name: BuildId + }, + { + Name: GetBuildIdGroup + } + ] + }, + { + Name: Invex.Atom.Build.BuildInfo.IBuildInfo, + Members: [ + { + Name: BuildId + }, + { + Name: BuildName + }, + { + Name: BuildSlice + }, + { + Name: BuildTimestamp + }, + { + Name: BuildVersion + } + ] + }, + { + Name: Invex.Atom.Build.BuildInfo.IBuildTimestampProvider, + Members: [ + { + Name: Timestamp + } + ] + }, + { + Name: Invex.Atom.Build.BuildInfo.IBuildVersionProvider, + Members: [ + { + Name: Version + } + ] + }, + { + Name: Invex.Atom.Build.BuildOptions.BuildOptionExtensions, + Members: [ + { + Name: Get + }, + { + Name: Get + }, + { + Name: GetOptions + }, + { + Name: GetOptions + }, + { + Name: GetOptionsGrouped + }, + { + Name: IsEnabled + }, + { + Name: IsEnabled + } + ] + }, + { + Name: Invex.Atom.Build.BuildOptions.BuildOptions + }, + { + Name: Invex.Atom.Build.BuildOptions.IBuildOption + }, + { + Name: Invex.Atom.Build.BuildOptions.IBuildOptionProvider, + Members: [ + { + Name: GetBuildOptions + } + ] + }, + { + Name: Invex.Atom.Build.BuildOptions.IBuildOptionService, + Members: [ + { + Name: Options + } + ] + }, + { + Name: Invex.Atom.Build.BuildOptions.ToggleBuildOption, + Members: [ + { + Name: $ + }, + { + Name: Enabled + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Definition.BuildDefinition, + Members: [ + { + Name: AccessParam + }, + { + Name: ConfigureDefinitionHost + }, + { + Name: ParamDefinitions + }, + { + Name: Services + }, + { + Name: TargetDefinitions + } + ] + }, + { + Name: Invex.Atom.Build.Definition.BuildDefinitionAttribute + }, + { + Name: Invex.Atom.Build.Definition.BuildDefinitionInterfaceAttribute + }, + { + Name: Invex.Atom.Build.Definition.DefinedParam, + Members: [ + { + Name: $ + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: Param + }, + { + Name: Required + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Definition.GenerateInterfaceMembersAttribute + }, + { + Name: Invex.Atom.Build.Definition.GenerateSolutionModelAttribute + }, + { + Name: Invex.Atom.Build.Definition.IBuildDefinition, + Members: [ + { + Name: AccessParam + }, + { + Name: ConfigureDefinitionHost + }, + { + Name: Options + }, + { + Name: ParamDefinitions + }, + { + Name: TargetDefinitions + } + ] + }, + { + Name: Invex.Atom.Build.Definition.Target, + Members: [ + { + Name: BeginInvoke + }, + { + Name: EndInvoke + }, + { + Name: Invoke + } + ] + }, + { + Name: Invex.Atom.Build.Definition.TargetDefinition, + Members: [ + { + Name: ConsumedArtifacts + }, + { + Name: ConsumedVariables + }, + { + Name: ConsumesArtifact + }, + { + Name: ConsumesArtifact + }, + { + Name: ConsumesArtifacts + }, + { + Name: ConsumesArtifacts + }, + { + Name: ConsumesVariable + }, + { + Name: Dependencies + }, + { + Name: DependsOn + }, + { + Name: DependsOn + }, + { + Name: DependsOn + }, + { + Name: DescribedAs + }, + { + Name: Description + }, + { + Name: Executes + }, + { + Name: Executes + }, + { + Name: Executes + }, + { + Name: Extends + }, + { + Name: Hidden + }, + { + Name: IsHidden + }, + { + Name: Name + }, + { + Name: Params + }, + { + Name: ProducedArtifacts + }, + { + Name: ProducedVariables + }, + { + Name: ProducesArtifact + }, + { + Name: ProducesArtifacts + }, + { + Name: ProducesVariable + }, + { + Name: RequiresParam + }, + { + Name: Tasks + }, + { + Name: UsesParam + } + ] + }, + { + Name: Invex.Atom.Build.Definition.TargetDefinitionExtensions, + Members: [ + { + Name: DependsOn + }, + { + Name: DependsOn + }, + { + Name: DependsOn + }, + { + Name: DependsOn + } + ] + }, + { + Name: Invex.Atom.Build.Exceptions.AtomException + }, + { + Name: Invex.Atom.Build.Exceptions.BuildConfigurationException, + Members: [ + { + Name: ReportData + } + ] + }, + { + Name: Invex.Atom.Build.Exceptions.CommandLineException, + Members: [ + { + Name: ArgumentName + } + ] + }, + { + Name: Invex.Atom.Build.Exceptions.StepFailedException, + Members: [ + { + Name: ReportData + } + ] + }, + { + Name: Invex.Atom.Build.FileSystem.AtomPaths, + Members: [ + { + Name: Artifacts + }, + { + Name: Publish + }, + { + Name: Root + }, + { + Name: Temp + } + ] + }, + { + Name: Invex.Atom.Build.FileSystem.FileSystemExtensions, + Members: [ + { + Name: get_AtomArtifactsDirectory + }, + { + Name: get_AtomPublishDirectory + }, + { + Name: get_AtomRootDirectory + }, + { + Name: get_AtomTempDirectory + }, + { + Name: get_CurrentDirectory + } + ] + }, + { + Name: Invex.Atom.Build.Help.IHelpService, + Members: [ + { + Name: ShowHelp + } + ] + }, + { + Name: Invex.Atom.Build.Hosting.AtomHost, + Members: [ + { + Name: CreateAtomBuilder + }, + { + Name: Run + } + ] + }, + { + Name: Invex.Atom.Build.Hosting.ConfigureHostAttribute + }, + { + Name: Invex.Atom.Build.Hosting.ConfigureHostBuilderAttribute + }, + { + Name: Invex.Atom.Build.Hosting.GenerateEntryPointAttribute + }, + { + Name: Invex.Atom.Build.Hosting.HostExtensions, + Members: [ + { + Name: AddAtom + }, + { + Name: UseAtom + } + ] + }, + { + Name: Invex.Atom.Build.Hosting.IConfigureHost, + Members: [ + { + Name: ConfigureBuildHost + }, + { + Name: ConfigureBuildHostBuilder + } + ] + }, + { + Name: Invex.Atom.Build.IAtomLifecycleHook, + Members: [ + { + Name: AfterExecute + }, + { + Name: BeforeExecute + } + ] + }, + { + Name: Invex.Atom.Build.IBuildAccessor, + Members: [ + { + Name: Services + } + ] + }, + { + Name: Invex.Atom.Build.ISetupBuildInfo, + Members: [ + { + Name: SetupBuildInfo + } + ] + }, + { + Name: Invex.Atom.Build.IValidateBuild, + Members: [ + { + Name: ValidateBuild + } + ] + }, + { + Name: Invex.Atom.Build.Logging.LogOptions, + Members: [ + { + Name: IsVerboseEnabled + } + ] + }, + { + Name: Invex.Atom.Build.Model.BuildModel, + Members: [ + { + Name: $ + }, + { + Name: CurrentTarget + }, + { + Name: DeclaringAssembly + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: GetTarget + }, + { + Name: GetTargetState + }, + { + Name: Targets + }, + { + Name: TargetStates + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Model.ConsumedArtifact, + Members: [ + { + Name: $ + }, + { + Name: ArtifactName + }, + { + Name: BuildSlice + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: TargetName + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Model.ConsumedVariable, + Members: [ + { + Name: $ + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: TargetName + }, + { + Name: ToString + }, + { + Name: VariableName + } + ] + }, + { + Name: Invex.Atom.Build.Model.ProducedArtifact, + Members: [ + { + Name: $ + }, + { + Name: ArtifactName + }, + { + Name: BuildSlice + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Model.TargetModel, + Members: [ + { + Name: $ + }, + { + Name: ConsumedArtifacts + }, + { + Name: ConsumedVariables + }, + { + Name: DeclaringAssembly + }, + { + Name: Deconstruct + }, + { + Name: Dependencies + }, + { + Name: Description + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: IsHidden + }, + { + Name: Name + }, + { + Name: Params + }, + { + Name: ProducedArtifacts + }, + { + Name: ProducedVariables + }, + { + Name: Tasks + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Model.TargetRunState, + Members: [ + { + Name: Failed + }, + { + Name: NotRun + }, + { + Name: PendingRun + }, + { + Name: Running + }, + { + Name: Skipped + }, + { + Name: Succeeded + }, + { + Name: Uninitialized + }, + { + Name: value__ + } + ] + }, + { + Name: Invex.Atom.Build.Model.TargetState, + Members: [ + { + Name: $ + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: Name + }, + { + Name: RunDuration + }, + { + Name: Status + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Model.UsedParam, + Members: [ + { + Name: $ + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: Param + }, + { + Name: Required + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Params.IParamService, + Members: [ + { + Name: CreateDefaultValuesOnlyScope + }, + { + Name: CreateNoCacheScope + }, + { + Name: CreateOverrideSourcesScope + }, + { + Name: GetParam + }, + { + Name: GetParam + }, + { + Name: GetParam + }, + { + Name: MaskMatchingSecrets + } + ] + }, + { + Name: Invex.Atom.Build.Params.ParamDefinition, + Members: [ + { + Name: $ + }, + { + Name: ArgName + }, + { + Name: ChainedParams + }, + { + Name: Deconstruct + }, + { + Name: Description + }, + { + Name: EnvVarName + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: IsSecret + }, + { + Name: Name + }, + { + Name: Sources + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Params.ParamDefinitionAttribute, + Members: [ + { + Name: ArgName + }, + { + Name: ChainedParams + }, + { + Name: Description + }, + { + Name: IsSecret + }, + { + Name: Sources + } + ] + }, + { + Name: Invex.Atom.Build.Params.ParamModel, + Members: [ + { + Name: $ + }, + { + Name: ArgName + }, + { + Name: ChainedParams + }, + { + Name: Deconstruct + }, + { + Name: DefaultValue + }, + { + Name: Description + }, + { + Name: EnvVarName + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: IsSecret + }, + { + Name: Name + }, + { + Name: Sources + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Params.ParamSource, + Members: [ + { + Name: All + }, + { + Name: Cache + }, + { + Name: CommandLineArgs + }, + { + Name: Configuration + }, + { + Name: EnvironmentVariables + }, + { + Name: None + }, + { + Name: Secrets + }, + { + Name: value__ + } + ] + }, + { + Name: Invex.Atom.Build.Params.SecretDefinitionAttribute + }, + { + Name: Invex.Atom.Build.Reports.ArtifactReportData, + Members: [ + { + Name: $ + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: Name + }, + { + Name: Path + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Reports.ColumnAlignment, + Members: [ + { + Name: Center + }, + { + Name: Left + }, + { + Name: Right + }, + { + Name: value__ + } + ] + }, + { + Name: Invex.Atom.Build.Reports.ICustomReportData, + Members: [ + { + Name: BeforeStandardData + } + ] + }, + { + Name: Invex.Atom.Build.Reports.IOutcomeReportWriter, + Members: [ + { + Name: ReportRunOutcome + } + ] + }, + { + Name: Invex.Atom.Build.Reports.IReportData + }, + { + Name: Invex.Atom.Build.Reports.IReportsHelper, + Members: [ + { + Name: AddReportData + } + ] + }, + { + Name: Invex.Atom.Build.Reports.ListReportData, + Members: [ + { + Name: $ + }, + { + Name: BeforeStandardData + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: Items + }, + { + Name: Prefix + }, + { + Name: Title + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Reports.LogReportData, + Members: [ + { + Name: $ + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: Exception + }, + { + Name: GetHashCode + }, + { + Name: Level + }, + { + Name: Message + }, + { + Name: Timestamp + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Reports.ReportDataMarkdownFormatter, + Members: [ + { + Name: Write + } + ] + }, + { + Name: Invex.Atom.Build.Reports.ReportService, + Members: [ + { + Name: AddReportData + }, + { + Name: GetReportData + } + ] + }, + { + Name: Invex.Atom.Build.Reports.TableReportData, + Members: [ + { + Name: $ + }, + { + Name: BeforeStandardData + }, + { + Name: ColumnAlignments + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: Header + }, + { + Name: Rows + }, + { + Name: Title + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Reports.TextReportData, + Members: [ + { + Name: $ + }, + { + Name: BeforeStandardData + }, + { + Name: Deconstruct + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: Text + }, + { + Name: Title + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Secrets.IDotnetUserSecrets + }, + { + Name: Invex.Atom.Build.Secrets.ISecretsProvider, + Members: [ + { + Name: GetSecret + }, + { + Name: Priority + } + ] + }, + { + Name: Invex.Atom.Build.Util.Scope.ActionScope, + Members: [ + { + Name: $ + }, + { + Name: Deconstruct + }, + { + Name: Dispose + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: OnDispose + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Util.Scope.NullScope, + Members: [ + { + Name: Dispose + }, + { + Name: Instance + } + ] + }, + { + Name: Invex.Atom.Build.Util.Scope.TaskScope, + Members: [ + { + Name: $ + }, + { + Name: Deconstruct + }, + { + Name: DisposeAsync + }, + { + Name: Equals + }, + { + Name: Equals + }, + { + Name: GetHashCode + }, + { + Name: OnDispose + }, + { + Name: ToString + } + ] + }, + { + Name: Invex.Atom.Build.Util.StringUtil, + Members: [ + { + Name: GetLevenshteinDistance + }, + { + Name: SanitizeForLogging + }, + { + Name: SanitizeSecrets + } + ] + }, + { + Name: Invex.Atom.Build.Util.TaskExtensions, + Members: [ + { + Name: WithRetry + }, + { + Name: WithRetry + }, + { + Name: WithRetry + }, + { + Name: WithRetry + } + ] + }, + { + Name: Invex.Atom.Build.Util.TypeUtil, + Members: [ + { + Name: Convert + } + ] + }, + { + Name: Invex.Atom.Build.Util.UnstableAPIAttribute + }, + { + Name: Invex.Atom.Build.Variables.IVariableProvider, + Members: [ + { + Name: ReadVariable + }, + { + Name: WriteVariable + } + ] + }, + { + Name: Invex.Atom.Build.Variables.IVariableService, + Members: [ + { + Name: ReadVariable + }, + { + Name: WriteVariable + } + ] + }, + { + Name: Invex.Atom.Build.Variables.IVariablesHelper, + Members: [ + { + Name: WriteVariable + } + ] + } +] \ No newline at end of file diff --git a/DecSm.Atom.Tests/ApiSurfaceTests/PublicApiSurfaceTests.cs b/tests/Invex.Atom.Build.Tests/ApiSurfaceTests/PublicApiSurfaceTests.cs similarity index 93% rename from DecSm.Atom.Tests/ApiSurfaceTests/PublicApiSurfaceTests.cs rename to tests/Invex.Atom.Build.Tests/ApiSurfaceTests/PublicApiSurfaceTests.cs index cd219999..0b94b250 100644 --- a/DecSm.Atom.Tests/ApiSurfaceTests/PublicApiSurfaceTests.cs +++ b/tests/Invex.Atom.Build.Tests/ApiSurfaceTests/PublicApiSurfaceTests.cs @@ -1,12 +1,12 @@ -namespace DecSm.Atom.Tests.ApiSurfaceTests; +namespace Invex.Atom.Build.Tests.ApiSurfaceTests; [TestFixture] -public class PublicApiSurfaceTests +internal sealed class PublicApiSurfaceTests { [Test] public async Task VerifyPublicApiSurface() { - // Get all types in DecSm.Atom assembly that are annotated with [PublicAPI] attribute + // Get all types in Invex.Atom assembly that are annotated with [PublicAPI] attribute var publicApiSurface = typeof(BuildDefinition) .Assembly .GetTypes() diff --git a/DecSm.Atom.Tests/BuildTests/BuildInfo/BuildInfoBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/BuildInfo/BuildInfoBuild.cs similarity index 81% rename from DecSm.Atom.Tests/BuildTests/BuildInfo/BuildInfoBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/BuildInfo/BuildInfoBuild.cs index 6c956703..36c5caf6 100644 --- a/DecSm.Atom.Tests/BuildTests/BuildInfo/BuildInfoBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/BuildInfo/BuildInfoBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.BuildInfo; +namespace Invex.Atom.Build.Tests.BuildTests.BuildInfo; [BuildDefinition] -public sealed partial class BuildInfoBuild : MinimalBuildDefinition, IBuildInfoTarget +public sealed partial class BuildInfoBuild : BuildDefinition, IBuildInfoTarget { public string? BuildNameResult { get; set; } diff --git a/DecSm.Atom.Tests/BuildTests/BuildInfo/BuildInfoTests.cs b/tests/Invex.Atom.Build.Tests/BuildTests/BuildInfo/BuildInfoTests.cs similarity index 87% rename from DecSm.Atom.Tests/BuildTests/BuildInfo/BuildInfoTests.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/BuildInfo/BuildInfoTests.cs index 3efe0392..8d63a7a8 100644 --- a/DecSm.Atom.Tests/BuildTests/BuildInfo/BuildInfoTests.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/BuildInfo/BuildInfoTests.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.BuildInfo; +namespace Invex.Atom.Build.Tests.BuildTests.BuildInfo; [TestFixture] -public class BuildInfoTests +internal sealed class BuildInfoTests { private sealed record TestTimestampProvider(long Timestamp = 12345678) : IBuildTimestampProvider; @@ -41,10 +41,10 @@ public async Task BuildInfo_Config_OverridesDefaultValues() b.Configuration.AddInMemoryCollection(new Dictionary { - { "Params:build-name", "TestBuildName" }, - { "Params:build-id", "TestBuildId" }, - { "Params:build-version", "2.1.3" }, - { "Params:build-timestamp", "87654321" }, + ["Params:build-name"] = "TestBuildName", + ["Params:build-id"] = "TestBuildId", + ["Params:build-version"] = "2.1.3", + ["Params:build-timestamp"] = "87654321", }); }); @@ -82,10 +82,10 @@ public async Task BuildInfo_EnvironmentVariable_OverridesConfig() b.Configuration.AddInMemoryCollection(new Dictionary { - { "Params:build-name", "ConfigBuildName" }, - { "Params:build-id", "ConfigBuildId" }, - { "Params:build-version", "3.2.1" }, - { "Params:build-timestamp", "11223344" }, + ["Params:build-name"] = "ConfigBuildName", + ["Params:build-id"] = "ConfigBuildId", + ["Params:build-version"] = "3.2.1", + ["Params:build-timestamp"] = "11223344", }); }); diff --git a/DecSm.Atom.Tests/BuildTests/Console/ConsoleBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleBuild.cs similarity index 93% rename from DecSm.Atom.Tests/BuildTests/Console/ConsoleBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleBuild.cs index a9745aec..626dbf21 100644 --- a/DecSm.Atom.Tests/BuildTests/Console/ConsoleBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleBuild.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Tests.BuildTests.Console; +namespace Invex.Atom.Build.Tests.BuildTests.Console; [BuildDefinition] public partial class ConsoleBuild : BuildDefinition, IConsoleTarget; diff --git a/DecSm.Atom.Tests/BuildTests/Console/ConsoleTests.ConsoleBuildDefinition_Displays_DefaultConsoleMessage.verified.txt b/tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleTests.ConsoleBuildDefinition_Displays_DefaultConsoleMessage.verified.txt similarity index 94% rename from DecSm.Atom.Tests/BuildTests/Console/ConsoleTests.ConsoleBuildDefinition_Displays_DefaultConsoleMessage.verified.txt rename to tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleTests.ConsoleBuildDefinition_Displays_DefaultConsoleMessage.verified.txt index f5eb1a3f..e4b78423 100644 --- a/DecSm.Atom.Tests/BuildTests/Console/ConsoleTests.ConsoleBuildDefinition_Displays_DefaultConsoleMessage.verified.txt +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleTests.ConsoleBuildDefinition_Displays_DefaultConsoleMessage.verified.txt @@ -8,12 +8,12 @@ Options -h, --help Show help for entire tool or a single command -i, --interactive Run in interactive mode (prompt for required params) - -g, --gen Generate build scripts -s, --skip Skip dependency execution (run only specified commands) -hl, --headless Run in headless mode (no prompts or logins, used in CI) -v, --verbose Show verbose output (extra logging) Project Commands +Name (Alias) | Description ConsoleTarget | Console target ├── Requires diff --git a/DecSm.Atom.Tests/BuildTests/Console/ConsoleTests.DefaultBuildDefinition_Displays_DefaultConsoleMessage.verified.txt b/tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleTests.DefaultBuildDefinition_Displays_DefaultConsoleMessage.verified.txt similarity index 90% rename from DecSm.Atom.Tests/BuildTests/Console/ConsoleTests.DefaultBuildDefinition_Displays_DefaultConsoleMessage.verified.txt rename to tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleTests.DefaultBuildDefinition_Displays_DefaultConsoleMessage.verified.txt index 3a4e2d44..7abe11e2 100644 --- a/DecSm.Atom.Tests/BuildTests/Console/ConsoleTests.DefaultBuildDefinition_Displays_DefaultConsoleMessage.verified.txt +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleTests.DefaultBuildDefinition_Displays_DefaultConsoleMessage.verified.txt @@ -8,7 +8,6 @@ Options -h, --help Show help for entire tool or a single command -i, --interactive Run in interactive mode (prompt for required params) - -g, --gen Generate build scripts -s, --skip Skip dependency execution (run only specified commands) -hl, --headless Run in headless mode (no prompts or logins, used in CI) -v, --verbose Show verbose output (extra logging) diff --git a/DecSm.Atom.Tests/BuildTests/Console/ConsoleTests.MinimalBuildDefinition_Displays_DefaultConsoleMessage.verified.txt b/tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleTests.MinimalBuildDefinition_Displays_DefaultConsoleMessage.verified.txt similarity index 90% rename from DecSm.Atom.Tests/BuildTests/Console/ConsoleTests.MinimalBuildDefinition_Displays_DefaultConsoleMessage.verified.txt rename to tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleTests.MinimalBuildDefinition_Displays_DefaultConsoleMessage.verified.txt index 3a4e2d44..7abe11e2 100644 --- a/DecSm.Atom.Tests/BuildTests/Console/ConsoleTests.MinimalBuildDefinition_Displays_DefaultConsoleMessage.verified.txt +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleTests.MinimalBuildDefinition_Displays_DefaultConsoleMessage.verified.txt @@ -8,7 +8,6 @@ Options -h, --help Show help for entire tool or a single command -i, --interactive Run in interactive mode (prompt for required params) - -g, --gen Generate build scripts -s, --skip Skip dependency execution (run only specified commands) -hl, --headless Run in headless mode (no prompts or logins, used in CI) -v, --verbose Show verbose output (extra logging) diff --git a/DecSm.Atom.Tests/BuildTests/Console/ConsoleTests.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleTests.cs similarity index 86% rename from DecSm.Atom.Tests/BuildTests/Console/ConsoleTests.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleTests.cs index 5a096949..c6d6954e 100644 --- a/DecSm.Atom.Tests/BuildTests/Console/ConsoleTests.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Console/ConsoleTests.cs @@ -1,14 +1,14 @@ -namespace DecSm.Atom.Tests.BuildTests.Console; +namespace Invex.Atom.Build.Tests.BuildTests.Console; [TestFixture] -public class ConsoleTests +internal sealed class ConsoleTests { [Test] public async Task MinimalBuildDefinition_Displays_DefaultConsoleMessage() { // Arrange var testConsole = new TestConsole(); - var host = CreateTestHost(testConsole); + var host = CreateTestHost(testConsole); // Act await host.RunAsync(); diff --git a/DecSm.Atom.Tests/BuildTests/Core/CoreTests.DefaultBuildDefinition_HasDefaultTargets.verified.txt b/tests/Invex.Atom.Build.Tests/BuildTests/Core/CoreTests.DefaultBuildDefinition_HasDefaultTargets.verified.txt similarity index 100% rename from DecSm.Atom.Tests/BuildTests/Core/CoreTests.DefaultBuildDefinition_HasDefaultTargets.verified.txt rename to tests/Invex.Atom.Build.Tests/BuildTests/Core/CoreTests.DefaultBuildDefinition_HasDefaultTargets.verified.txt diff --git a/tests/Invex.Atom.Build.Tests/BuildTests/Core/CoreTests.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Core/CoreTests.cs new file mode 100644 index 00000000..5049ceae --- /dev/null +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Core/CoreTests.cs @@ -0,0 +1,20 @@ +namespace Invex.Atom.Build.Tests.BuildTests.Core; + +[TestFixture] +internal sealed class CoreTests +{ + [Test] + public void DefaultBuildDefinition_IsEmpty() + { + // Arrange + var host = CreateTestHost(); + + // Act + var buildModel = host.Services.GetRequiredService(); + + // Assert + buildModel.ShouldSatisfyAllConditions(b => b.Targets.ShouldBeEmpty(), + b => b.TargetStates.ShouldBeEmpty(), + b => b.CurrentTarget.ShouldBeNull()); + } +} diff --git a/DecSm.Atom.Tests/BuildTests/Core/DefaultAtomBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Core/DefaultAtomBuild.cs similarity index 61% rename from DecSm.Atom.Tests/BuildTests/Core/DefaultAtomBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Core/DefaultAtomBuild.cs index 69596302..b1c25178 100644 --- a/DecSm.Atom.Tests/BuildTests/Core/DefaultAtomBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Core/DefaultAtomBuild.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Tests.BuildTests.Core; +namespace Invex.Atom.Build.Tests.BuildTests.Core; [BuildDefinition] public sealed partial class DefaultAtomBuild : BuildDefinition; diff --git a/DecSm.Atom.Tests/BuildTests/Core/TestTargetAtomBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Core/TestTargetAtomBuild.cs similarity index 74% rename from DecSm.Atom.Tests/BuildTests/Core/TestTargetAtomBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Core/TestTargetAtomBuild.cs index 6a289fa8..d57f4629 100644 --- a/DecSm.Atom.Tests/BuildTests/Core/TestTargetAtomBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Core/TestTargetAtomBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.Core; +namespace Invex.Atom.Build.Tests.BuildTests.Core; [BuildDefinition] -public partial class TestTargetAtomBuild : MinimalBuildDefinition, ITestTarget +public partial class TestTargetAtomBuild : BuildDefinition, ITestTarget { public string Description { get; set; } = "Test target"; diff --git a/DecSm.Atom.Tests/BuildTests/FileSystem/FileSystemTests.cs b/tests/Invex.Atom.Build.Tests/BuildTests/FileSystem/FileSystemTests.cs similarity index 66% rename from DecSm.Atom.Tests/BuildTests/FileSystem/FileSystemTests.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/FileSystem/FileSystemTests.cs index 7f0a7397..9130913b 100644 --- a/DecSm.Atom.Tests/BuildTests/FileSystem/FileSystemTests.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/FileSystem/FileSystemTests.cs @@ -1,16 +1,16 @@ -namespace DecSm.Atom.Tests.BuildTests.FileSystem; +namespace Invex.Atom.Build.Tests.BuildTests.FileSystem; [TestFixture] public sealed class FileSystemTests { [Test] - public void Minimal_BuildDefinition_WithDefaultLocation_Locates_AtomRootDirectory() + public void Default_BuildDefinition_WithDefaultLocation_Locates_AtomRootDirectory() { // Arrange - var host = CreateTestHost(); + var host = CreateTestHost(); // Act - var atomFileSystem = host.Services.GetRequiredService(); + var atomFileSystem = host.Services.GetRequiredService(); var atomRootDirectory = atomFileSystem.AtomRootDirectory; // Assert @@ -22,24 +22,24 @@ public void Minimal_BuildDefinition_WithDefaultLocation_Locates_AtomRootDirector } [Test] - public void Minimal_BuildDefinition_WithCustomLocator_Locates_AtomRootDirectory() + public void Default_BuildDefinition_WithCustomLocator_Locates_AtomRootDirectory() { // Arrange - var host = CreateTestHost(configure: builder => + var host = CreateTestHost(configure: builder => builder.Services.AddSingleton(provider => new FunctionPathProvider { - Resolver = key => key is AtomPaths.Root + Provider = key => key is AtomPaths.Root ? provider - .GetRequiredService() + .GetRequiredService() .CreateRootedPath(Environment.OSVersion.Platform is PlatformID.Win32NT ? @"C:\CustomAtomRoot" : "/CustomAtomRoot") : null, - Priority = 0, + Priority = 1, })); // Act - var atomFileSystem = host.Services.GetRequiredService(); + var atomFileSystem = host.Services.GetRequiredService(); var atomRootDirectory = atomFileSystem.AtomRootDirectory; // Assert @@ -51,13 +51,13 @@ public void Minimal_BuildDefinition_WithCustomLocator_Locates_AtomRootDirectory( } [Test] - public void Minimal_BuildDefinition_WithDefaultLocation_Locates_AtomArtifactsDirectory() + public void Default_BuildDefinition_WithDefaultLocation_Locates_AtomArtifactsDirectory() { // Arrange - var host = CreateTestHost(); + var host = CreateTestHost(); // Act - var atomFileSystem = host.Services.GetRequiredService(); + var atomFileSystem = host.Services.GetRequiredService(); var atomRootDirectory = atomFileSystem.AtomArtifactsDirectory; // Assert @@ -69,24 +69,24 @@ public void Minimal_BuildDefinition_WithDefaultLocation_Locates_AtomArtifactsDir } [Test] - public void Minimal_BuildDefinition_WithCustomLocator_Locates_AtomArtifactsDirectory() + public void Default_BuildDefinition_WithCustomLocator_Locates_AtomArtifactsDirectory() { // Arrange - var host = CreateTestHost(configure: builder => + var host = CreateTestHost(configure: builder => builder.Services.AddSingleton(provider => new FunctionPathProvider { - Resolver = key => key is AtomPaths.Artifacts + Provider = key => key is AtomPaths.Artifacts ? provider - .GetRequiredService() + .GetRequiredService() .CreateRootedPath(Environment.OSVersion.Platform is PlatformID.Win32NT ? @"C:\CustomAtomArtifacts" : "/CustomAtomArtifacts") : null, - Priority = 0, + Priority = 1, })); // Act - var atomFileSystem = host.Services.GetRequiredService(); + var atomFileSystem = host.Services.GetRequiredService(); var atomRootDirectory = atomFileSystem.AtomArtifactsDirectory; // Assert @@ -98,13 +98,13 @@ public void Minimal_BuildDefinition_WithCustomLocator_Locates_AtomArtifactsDirec } [Test] - public void Minimal_BuildDefinition_WithDefaultLocation_Locates_AtomPublishDirectory() + public void Default_BuildDefinition_WithDefaultLocation_Locates_AtomPublishDirectory() { // Arrange - var host = CreateTestHost(); + var host = CreateTestHost(); // Act - var atomFileSystem = host.Services.GetRequiredService(); + var atomFileSystem = host.Services.GetRequiredService(); var atomRootDirectory = atomFileSystem.AtomPublishDirectory; // Assert @@ -116,24 +116,24 @@ public void Minimal_BuildDefinition_WithDefaultLocation_Locates_AtomPublishDirec } [Test] - public void Minimal_BuildDefinition_WithCustomLocator_Locates_AtomPublishDirectory() + public void Default_BuildDefinition_WithCustomLocator_Locates_AtomPublishDirectory() { // Arrange - var host = CreateTestHost(configure: builder => + var host = CreateTestHost(configure: builder => builder.Services.AddSingleton(provider => new FunctionPathProvider { - Resolver = key => key is AtomPaths.Publish + Provider = key => key is AtomPaths.Publish ? provider - .GetRequiredService() + .GetRequiredService() .CreateRootedPath(Environment.OSVersion.Platform is PlatformID.Win32NT ? @"C:\CustomAtomPublish" : "/CustomAtomPublish") : null, - Priority = 0, + Priority = 1, })); // Act - var atomFileSystem = host.Services.GetRequiredService(); + var atomFileSystem = host.Services.GetRequiredService(); var atomRootDirectory = atomFileSystem.AtomPublishDirectory; // Assert @@ -145,13 +145,13 @@ public void Minimal_BuildDefinition_WithCustomLocator_Locates_AtomPublishDirecto } [Test] - public void Minimal_BuildDefinition_WithDefaultLocation_Locates_AtomTempDirectory() + public void Default_BuildDefinition_WithDefaultLocation_Locates_AtomTempDirectory() { // Arrange - var host = CreateTestHost(); + var host = CreateTestHost(); // Act - var atomFileSystem = host.Services.GetRequiredService(); + var atomFileSystem = host.Services.GetRequiredService(); var atomRootDirectory = atomFileSystem.AtomTempDirectory; // Assert @@ -163,24 +163,24 @@ public void Minimal_BuildDefinition_WithDefaultLocation_Locates_AtomTempDirector } [Test] - public void Minimal_BuildDefinition_WithCustomLocator_Locates_AtomTempDirectory() + public void Default_BuildDefinition_WithCustomLocator_Locates_AtomTempDirectory() { // Arrange - var host = CreateTestHost(configure: builder => + var host = CreateTestHost(configure: builder => builder.Services.AddSingleton(provider => new FunctionPathProvider { - Resolver = key => key is AtomPaths.Temp + Provider = key => key is AtomPaths.Temp ? provider - .GetRequiredService() + .GetRequiredService() .CreateRootedPath(Environment.OSVersion.Platform is PlatformID.Win32NT ? @"C:\CustomAtomTemp" : "/CustomAtomTemp") : null, - Priority = 0, + Priority = 1, })); // Act - var atomFileSystem = host.Services.GetRequiredService(); + var atomFileSystem = host.Services.GetRequiredService(); var atomRootDirectory = atomFileSystem.AtomTempDirectory; // Assert diff --git a/DecSm.Atom.Tests/BuildTests/Params/ChainedParamBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Params/ChainedParamBuild.cs similarity index 80% rename from DecSm.Atom.Tests/BuildTests/Params/ChainedParamBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Params/ChainedParamBuild.cs index ca26350b..58f0823b 100644 --- a/DecSm.Atom.Tests/BuildTests/Params/ChainedParamBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Params/ChainedParamBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.Params; +namespace Invex.Atom.Build.Tests.BuildTests.Params; [BuildDefinition] -public partial class ChainedParamBuild : MinimalBuildDefinition, IChainedParamTarget; +public partial class ChainedParamBuild : BuildDefinition, IChainedParamTarget; public interface IChainedParamTarget : IBuildAccessor { diff --git a/DecSm.Atom.Tests/BuildTests/Params/OptionalParamBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Params/OptionalParamBuild.cs similarity index 84% rename from DecSm.Atom.Tests/BuildTests/Params/OptionalParamBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Params/OptionalParamBuild.cs index ba34c714..f4af18c4 100644 --- a/DecSm.Atom.Tests/BuildTests/Params/OptionalParamBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Params/OptionalParamBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.Params; +namespace Invex.Atom.Build.Tests.BuildTests.Params; [BuildDefinition] -public partial class OptionalParamBuild : MinimalBuildDefinition, IOptionalParamTarget1 +public partial class OptionalParamBuild : BuildDefinition, IOptionalParamTarget1 { public string? ExecuteValue1 { get; set; } diff --git a/DecSm.Atom.Tests/BuildTests/Params/ParamBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Params/ParamBuild.cs similarity index 85% rename from DecSm.Atom.Tests/BuildTests/Params/ParamBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Params/ParamBuild.cs index e6872520..b828c36e 100644 --- a/DecSm.Atom.Tests/BuildTests/Params/ParamBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Params/ParamBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.Params; +namespace Invex.Atom.Build.Tests.BuildTests.Params; [BuildDefinition] -public partial class ParamBuild : MinimalBuildDefinition, IParamTarget1, IParamTarget2 +public partial class ParamBuild : BuildDefinition, IParamTarget1, IParamTarget2 { public string? ExecuteValue { get; set; } } diff --git a/DecSm.Atom.Tests/BuildTests/Params/ParamTests.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Params/ParamTests.cs similarity index 97% rename from DecSm.Atom.Tests/BuildTests/Params/ParamTests.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Params/ParamTests.cs index 3ab9dffa..0513fe18 100644 --- a/DecSm.Atom.Tests/BuildTests/Params/ParamTests.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Params/ParamTests.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.Params; +namespace Invex.Atom.Build.Tests.BuildTests.Params; [TestFixture] -public class ParamTests +internal sealed class ParamTests { [Test] public void Param_IsReadFromCommandLine() diff --git a/DecSm.Atom.Tests/BuildTests/Secrets/UserSecretsBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Secrets/UserSecretsBuild.cs similarity index 78% rename from DecSm.Atom.Tests/BuildTests/Secrets/UserSecretsBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Secrets/UserSecretsBuild.cs index fa692e04..b2bff6f2 100644 --- a/DecSm.Atom.Tests/BuildTests/Secrets/UserSecretsBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Secrets/UserSecretsBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.Secrets; +namespace Invex.Atom.Build.Tests.BuildTests.Secrets; [BuildDefinition] -public partial class UserSecretsBuild : MinimalBuildDefinition, IUserSecretsTarget, IDotnetUserSecrets +public partial class UserSecretsBuild : BuildDefinition, IUserSecretsTarget, IDotnetUserSecrets { public string? ExecutionValue { get; set; } } diff --git a/DecSm.Atom.Tests/BuildTests/Secrets/UserSecretsVaultTests.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Secrets/UserSecretsVaultTests.cs similarity index 94% rename from DecSm.Atom.Tests/BuildTests/Secrets/UserSecretsVaultTests.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Secrets/UserSecretsVaultTests.cs index 38b074d8..94203f47 100644 --- a/DecSm.Atom.Tests/BuildTests/Secrets/UserSecretsVaultTests.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Secrets/UserSecretsVaultTests.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.Secrets; +namespace Invex.Atom.Build.Tests.BuildTests.Secrets; [TestFixture] -public class UserSecretsVaultTests +internal sealed class UserSecretsVaultTests { [Test] public void UserSecretsVault_WhenMatch_ReturnsSecret() diff --git a/DecSm.Atom.Tests/BuildTests/Targets/CircularTargetDependencyBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/CircularTargetDependencyBuild.cs similarity index 89% rename from DecSm.Atom.Tests/BuildTests/Targets/CircularTargetDependencyBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Targets/CircularTargetDependencyBuild.cs index 0edca991..c71ef907 100644 --- a/DecSm.Atom.Tests/BuildTests/Targets/CircularTargetDependencyBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/CircularTargetDependencyBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.Targets; +namespace Invex.Atom.Build.Tests.BuildTests.Targets; [BuildDefinition] -public partial class CircularTargetDependencyBuild : MinimalBuildDefinition, ICircularTarget1, ICircularTarget2 +public partial class CircularTargetDependencyBuild : BuildDefinition, ICircularTarget1, ICircularTarget2 { public bool CircularTarget1Executed { get; set; } @@ -39,7 +39,7 @@ public interface ICircularTarget2 } [BuildDefinition] -public partial class CircularTargetDependencyBuild2 : MinimalBuildDefinition, +public partial class CircularTargetDependencyBuild2 : BuildDefinition, ITestCircularTarget3, ITestCircularTarget4, ITestCircularTarget5 diff --git a/DecSm.Atom.Tests/BuildTests/Targets/ConfigureBuilderAndHostBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/ConfigureBuilderAndHostBuild.cs similarity index 63% rename from DecSm.Atom.Tests/BuildTests/Targets/ConfigureBuilderAndHostBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Targets/ConfigureBuilderAndHostBuild.cs index d716e97a..ed735ceb 100644 --- a/DecSm.Atom.Tests/BuildTests/Targets/ConfigureBuilderAndHostBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/ConfigureBuilderAndHostBuild.cs @@ -1,8 +1,8 @@ -namespace DecSm.Atom.Tests.BuildTests.Targets; +namespace Invex.Atom.Build.Tests.BuildTests.Targets; // ReSharper disable once RedundantExtendsListEntry [BuildDefinition] -public partial class ConfigureBuilderAndHostBuild : MinimalBuildDefinition, +public partial class ConfigureBuilderAndHostBuild : BuildDefinition, ITargetWithConfigureBuilder, ITargetWithConfigureBuilderAndConfigureHost, ITargetWithInheritAndConfigureBuilderAndConfigureHost @@ -15,7 +15,7 @@ public partial class ConfigureBuilderAndHostBuild : MinimalBuildDefinition, [ConfigureHostBuilder] public partial interface ITargetWithConfigureBuilder { - protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) => + protected static partial void ConfigureBuilderFromITargetWithConfigureBuilder(IHostApplicationBuilder builder) => builder.Configuration.AddInMemoryCollection([new("SetupExecuted1", "true")]); } @@ -25,10 +25,11 @@ public partial interface ITargetWithConfigureBuilderAndConfigureHost { bool IsSetupExecuted2 { get; set; } - protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) => + protected static partial void ConfigureBuilderFromITargetWithConfigureBuilderAndConfigureHost( + IHostApplicationBuilder builder) => builder.Configuration.AddInMemoryCollection([new("SetupExecuted2", "true")]); - protected static partial void ConfigureHost(IHost host) => + protected static partial void ConfigureHostFromITargetWithConfigureBuilderAndConfigureHost(IHost host) => ((ITargetWithConfigureBuilderAndConfigureHost)host.Services.GetRequiredService()) .IsSetupExecuted2 = true; } @@ -40,10 +41,11 @@ public partial interface { bool IsSetupExecuted3 { get; set; } - protected new static partial void ConfigureBuilder(IHostApplicationBuilder builder) => + protected static partial void ConfigureBuilderFromITargetWithInheritAndConfigureBuilderAndConfigureHost( + IHostApplicationBuilder builder) => builder.Configuration.AddInMemoryCollection([new("SetupExecuted3", "true")]); - protected new static partial void ConfigureHost(IHost host) => + protected static partial void ConfigureHostFromITargetWithInheritAndConfigureBuilderAndConfigureHost(IHost host) => ((ITargetWithInheritAndConfigureBuilderAndConfigureHost)host.Services.GetRequiredService()) .IsSetupExecuted3 = true; } diff --git a/DecSm.Atom.Tests/BuildTests/Targets/DependencyTargetBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/DependencyTargetBuild.cs similarity index 93% rename from DecSm.Atom.Tests/BuildTests/Targets/DependencyTargetBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Targets/DependencyTargetBuild.cs index 862a3ecb..097f3780 100644 --- a/DecSm.Atom.Tests/BuildTests/Targets/DependencyTargetBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/DependencyTargetBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.Targets; +namespace Invex.Atom.Build.Tests.BuildTests.Targets; [BuildDefinition] -public partial class DependencyTargetBuild : MinimalBuildDefinition, +public partial class DependencyTargetBuild : BuildDefinition, IDependencyTarget1, IDependencyTarget2, IDependencyFailTarget1, diff --git a/tests/Invex.Atom.Build.Tests/BuildTests/Targets/DuplicateArtifactsBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/DuplicateArtifactsBuild.cs new file mode 100644 index 00000000..6e57659a --- /dev/null +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/DuplicateArtifactsBuild.cs @@ -0,0 +1,39 @@ +namespace Invex.Atom.Build.Tests.BuildTests.Targets; + +[BuildDefinition] +public partial class DuplicateArtifactsBuild : BuildDefinition, + IDuplicateArtifactsBase1, + IDuplicateArtifactsBase2, + IDuplicateArtifactsTarget; + +public interface IDuplicateArtifactsBase1 +{ + Target ProducerTarget => + t => t + .ProducesArtifact("TestArtifact") + .Executes(() => Task.CompletedTask); + + Target BaseTarget1 => + t => t + .ConsumesArtifact("ProducerTarget", "TestArtifact") + .ProducesArtifact("OutputArtifact") + .Executes(() => Task.CompletedTask); +} + +public interface IDuplicateArtifactsBase2 +{ + Target BaseTarget2 => + t => t + .ConsumesArtifact("ProducerTarget", "TestArtifact") + .ProducesArtifact("OutputArtifact") + .Executes(() => Task.CompletedTask); +} + +public interface IDuplicateArtifactsTarget +{ + Target DuplicateArtifactsTarget => + t => t + .Extends(d => d.BaseTarget1) + .Extends(d => d.BaseTarget2) + .Executes(() => Task.CompletedTask); +} diff --git a/tests/Invex.Atom.Build.Tests/BuildTests/Targets/DuplicateDependenciesBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/DuplicateDependenciesBuild.cs new file mode 100644 index 00000000..238c8cb6 --- /dev/null +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/DuplicateDependenciesBuild.cs @@ -0,0 +1,34 @@ +namespace Invex.Atom.Build.Tests.BuildTests.Targets; + +[BuildDefinition] +public partial class DuplicateDependenciesBuild : BuildDefinition, + IDuplicateDependenciesBase1, + IDuplicateDependenciesBase2, + IDuplicateDependenciesTarget; + +public interface IDuplicateDependenciesBase1 +{ + Target BaseDependency => t => t.Executes(() => Task.CompletedTask); + + Target BaseTarget1 => + t => t + .DependsOn("BaseDependency") + .Executes(() => Task.CompletedTask); +} + +public interface IDuplicateDependenciesBase2 +{ + Target BaseTarget2 => + t => t + .DependsOn("BaseDependency") + .Executes(() => Task.CompletedTask); +} + +public interface IDuplicateDependenciesTarget +{ + Target DuplicateDependenciesTarget => + t => t + .Extends(d => d.BaseTarget1) + .Extends(d => d.BaseTarget2) + .Executes(() => Task.CompletedTask); +} diff --git a/tests/Invex.Atom.Build.Tests/BuildTests/Targets/DuplicateVariablesBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/DuplicateVariablesBuild.cs new file mode 100644 index 00000000..544b9e16 --- /dev/null +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/DuplicateVariablesBuild.cs @@ -0,0 +1,39 @@ +namespace Invex.Atom.Build.Tests.BuildTests.Targets; + +[BuildDefinition] +public partial class DuplicateVariablesBuild : BuildDefinition, + IDuplicateVariablesBase1, + IDuplicateVariablesBase2, + IDuplicateVariablesTarget; + +public interface IDuplicateVariablesBase1 +{ + Target ProducerTarget => + t => t + .ProducesVariable("TestVariable") + .Executes(() => Task.CompletedTask); + + Target BaseTarget1 => + t => t + .ConsumesVariable("ProducerTarget", "TestVariable") + .ProducesVariable("OutputVariable") + .Executes(() => Task.CompletedTask); +} + +public interface IDuplicateVariablesBase2 +{ + Target BaseTarget2 => + t => t + .ConsumesVariable("ProducerTarget", "TestVariable") + .ProducesVariable("OutputVariable") + .Executes(() => Task.CompletedTask); +} + +public interface IDuplicateVariablesTarget +{ + Target DuplicateVariablesTarget => + t => t + .Extends(d => d.BaseTarget1) + .Extends(d => d.BaseTarget2) + .Executes(() => Task.CompletedTask); +} diff --git a/DecSm.Atom.Tests/BuildTests/Targets/ExtensionTargetBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/ExtensionTargetBuild.cs similarity index 83% rename from DecSm.Atom.Tests/BuildTests/Targets/ExtensionTargetBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Targets/ExtensionTargetBuild.cs index b3e8ab0e..8bf6574c 100644 --- a/DecSm.Atom.Tests/BuildTests/Targets/ExtensionTargetBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/ExtensionTargetBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.Targets; +namespace Invex.Atom.Build.Tests.BuildTests.Targets; [BuildDefinition] -public partial class ExtensionTargetBuild : MinimalBuildDefinition, IBaseExtensionTarget, IExtendedExtensionTarget +public partial class ExtensionTargetBuild : BuildDefinition, IBaseExtensionTarget, IExtendedExtensionTarget { public bool BaseExtensionTargetExecuted { get; set; } diff --git a/DecSm.Atom.Tests/BuildTests/Targets/TargetOverrideBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/TargetOverrideBuild.cs similarity index 83% rename from DecSm.Atom.Tests/BuildTests/Targets/TargetOverrideBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Targets/TargetOverrideBuild.cs index b8ae38a1..d3285891 100644 --- a/DecSm.Atom.Tests/BuildTests/Targets/TargetOverrideBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/TargetOverrideBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.Targets; +namespace Invex.Atom.Build.Tests.BuildTests.Targets; [BuildDefinition] -public partial class TargetOverrideBuild : MinimalBuildDefinition, IOverrideTarget +public partial class TargetOverrideBuild : BuildDefinition, IOverrideTarget { public bool BaseOverrideTargetExecuted { get; set; } diff --git a/DecSm.Atom.Tests/BuildTests/Targets/TargetTests.Build_WithUnspecifiedTargets_IncludesUnspecifiedTargets.verified.txt b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/TargetTests.Build_WithUnspecifiedTargets_IncludesUnspecifiedTargets.verified.txt similarity index 100% rename from DecSm.Atom.Tests/BuildTests/Targets/TargetTests.Build_WithUnspecifiedTargets_IncludesUnspecifiedTargets.verified.txt rename to tests/Invex.Atom.Build.Tests/BuildTests/Targets/TargetTests.Build_WithUnspecifiedTargets_IncludesUnspecifiedTargets.verified.txt diff --git a/DecSm.Atom.Tests/BuildTests/Targets/TargetTests.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/TargetTests.cs similarity index 64% rename from DecSm.Atom.Tests/BuildTests/Targets/TargetTests.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Targets/TargetTests.cs index 6df6f3c4..5e50ec9e 100644 --- a/DecSm.Atom.Tests/BuildTests/Targets/TargetTests.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/TargetTests.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.Targets; +namespace Invex.Atom.Build.Tests.BuildTests.Targets; [TestFixture] -public class TargetTests +internal sealed class TargetTests { [Test] public void TestTargetBuild_WithTestTargetArg_Executes_TestTarget() @@ -214,4 +214,103 @@ public void Build_WithConfigureHost_ExecutesSetupMethods() target.IsSetupExecuted2.ShouldBeTrue(); target.IsSetupExecuted3.ShouldBeTrue(); } + + [Test] + public void ApplyExtensions_WithDuplicateDependencies_DeduplicatesThem() + { + // Arrange + var host = CreateTestHost(commandLineArgs: new(true, + [new CommandArg(nameof(IDuplicateDependenciesTarget.DuplicateDependenciesTarget))])); + + // Act + var build = host.Services.GetRequiredService(); + + // Assert + var target = + build.Targets.Single(t => t.Name == nameof(IDuplicateDependenciesTarget.DuplicateDependenciesTarget)); + + target.Dependencies.Count.ShouldBe(1, "Dependencies should be deduplicated"); + } + + [Test] + public void ApplyExtensions_WithDuplicateArtifacts_DeduplicatesThem() + { + // Arrange + var host = CreateTestHost(commandLineArgs: new(true, + [new CommandArg(nameof(IDuplicateArtifactsTarget.DuplicateArtifactsTarget))])); + + // Act + var build = host.Services.GetRequiredService(); + + // Assert + var target = build.Targets.Single(t => t.Name == nameof(IDuplicateArtifactsTarget.DuplicateArtifactsTarget)); + target.ConsumedArtifacts.Count.ShouldBe(1, "Consumed artifacts should be deduplicated"); + target.ProducedArtifacts.Count.ShouldBe(1, "Produced artifacts should be deduplicated"); + } + + [Test] + public void ApplyExtensions_WithDuplicateVariables_DeduplicatesThem() + { + // Arrange + var host = CreateTestHost(commandLineArgs: new(true, + [new CommandArg(nameof(IDuplicateVariablesTarget.DuplicateVariablesTarget))])); + + // Act + var build = host.Services.GetRequiredService(); + + // Assert + var target = build.Targets.Single(t => t.Name == nameof(IDuplicateVariablesTarget.DuplicateVariablesTarget)); + target.ConsumedVariables.Count.ShouldBe(1, "Consumed variables should be deduplicated"); + target.ProducedVariables.Count.ShouldBe(1, "Produced variables should be deduplicated"); + } + + [Test] + public async Task TargetBuild_WithNoTargetsInvoked_DisplaysHelp() + { + // Arrange + var testConsole = new TestConsole(); + + var host = CreateTestHost(testConsole); + + // Act + await host.RunAsync(); + + // Assert + testConsole.Output.ShouldContain("Usage"); + testConsole.Output.ShouldContain("Options"); + } + + [Test] + public async Task TargetBuild_WithTargetInvoked_DoesNotDisplayHelp() + { + // Arrange + var testConsole = new TestConsole(); + + var host = CreateTestHost(testConsole, + commandLineArgs: new(true, [new CommandArg("TestTarget")])); + + // Act + await host.RunAsync(); + + // Assert + testConsole.Output.ShouldNotContain("Usage"); + testConsole.Output.ShouldNotContain("Options"); + } + + [Test] + public async Task TargetBuild_WithTargetInvokedAndHelpFlagPresent_DisplaysHelp() + { + // Arrange + var testConsole = new TestConsole(); + + var host = CreateTestHost(testConsole, + commandLineArgs: new(true, [new CommandArg("TestTarget"), new HelpArg()])); + + // Act + await host.RunAsync(); + + // Assert + testConsole.Output.ShouldContain("Usage"); + testConsole.Output.ShouldContain("Options"); + } } diff --git a/DecSm.Atom.Tests/BuildTests/Targets/UnspecifiedTargetsBuild.cs b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/UnspecifiedTargetsBuild.cs similarity index 81% rename from DecSm.Atom.Tests/BuildTests/Targets/UnspecifiedTargetsBuild.cs rename to tests/Invex.Atom.Build.Tests/BuildTests/Targets/UnspecifiedTargetsBuild.cs index 78050316..f45d3093 100644 --- a/DecSm.Atom.Tests/BuildTests/Targets/UnspecifiedTargetsBuild.cs +++ b/tests/Invex.Atom.Build.Tests/BuildTests/Targets/UnspecifiedTargetsBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.BuildTests.Targets; +namespace Invex.Atom.Build.Tests.BuildTests.Targets; [BuildDefinition] -internal partial class UnspecifiedTargetsBuild : MinimalBuildDefinition, +internal partial class UnspecifiedTargetsBuild : BuildDefinition, IUnspecifiedTarget1, IUnspecifiedTarget2, IUnspecifiedTarget3; diff --git a/DecSm.Atom.Tests/ClassTests/Args/CommandLineArgParserTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Args/CommandLineArgParserTests.cs similarity index 95% rename from DecSm.Atom.Tests/ClassTests/Args/CommandLineArgParserTests.cs rename to tests/Invex.Atom.Build.Tests/ClassTests/Args/CommandLineArgParserTests.cs index d172b4a8..d2eca557 100644 --- a/DecSm.Atom.Tests/ClassTests/Args/CommandLineArgParserTests.cs +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Args/CommandLineArgParserTests.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.ClassTests.Args; +namespace Invex.Atom.Build.Tests.ClassTests.Args; [TestFixture] -public class CommandLineArgParserTests +internal sealed class CommandLineArgParserTests { [Test] public void CommandLineArgsParser_Parses_NoArgs() @@ -42,29 +42,6 @@ public void CommandLineArgsParser_Parses_HelpArg(string arg) .ShouldBeOfType(); } - [TestCase("-g")] - [TestCase("-G")] - [TestCase("--gen")] - [TestCase("--GEN")] - public void CommandLineArgsParser_Parses_GenArg(string arg) - { - // Arrange - string[] rawArgs = [arg]; - var build = A.Fake(); - var console = new TestConsole(); - var parser = new CommandLineArgsParser(build, console); - - // Act - var parsedArgs = parser.Parse(rawArgs); - - // Assert - parsedArgs.Args.ShouldHaveSingleItem(); - - parsedArgs - .Args[0] - .ShouldBeOfType(); - } - [TestCase("-s")] [TestCase("-S")] [TestCase("--skip")] @@ -158,6 +135,22 @@ public void CommandLineArgsParser_Parses_ProjectArg(string arg, string value) .ShouldSatisfyAllConditions(x => x.ProjectName.ShouldBe(value)); } + [TestCase("-p")] + [TestCase("--project")] + public void CommandLineArgsParser_ProjectArg_MissingValue_ThrowsException(string arg) + { + // Arrange + string[] rawArgs = [arg]; + var build = A.Fake(); + var console = new TestConsole(); + var parser = new CommandLineArgsParser(build, console); + + // Act / Assert + var ex = Should.Throw(() => parser.Parse(rawArgs)); + ex.ArgumentName.ShouldBe("project"); + ex.Message.ShouldContain("Missing value"); + } + [TestCase("--param1", "param1", "Param1")] [TestCase("--PARAM1", "param1", "Param1")] [TestCase("--param2", "param2", "Param2")] @@ -256,7 +249,9 @@ public void CommandLineArgsParser_Parses_ParamWithoutValue() var parser = new CommandLineArgsParser(build, console); // Act / Assert - Should.Throw(() => parser.Parse(rawArgs)); + var ex = Should.Throw(() => parser.Parse(rawArgs)); + ex.ArgumentName.ShouldBe("param1"); + ex.Message.ShouldContain("Missing value for parameter"); } [Test] @@ -300,7 +295,9 @@ public void CommandLineArgsParser_Parses_ParamAtEnd() var parser = new CommandLineArgsParser(build, console); // Act / Assert - Should.Throw(() => parser.Parse(rawArgs)); + var ex = Should.Throw(() => parser.Parse(rawArgs)); + ex.ArgumentName.ShouldBe("param1"); + ex.Message.ShouldContain("Missing value for parameter"); } [TestCase("Command1", "Command1")] diff --git a/DecSm.Atom.Tests/ClassTests/Args/CommandLineArgsTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Args/CommandLineArgsTests.cs similarity index 64% rename from DecSm.Atom.Tests/ClassTests/Args/CommandLineArgsTests.cs rename to tests/Invex.Atom.Build.Tests/ClassTests/Args/CommandLineArgsTests.cs index 9a758660..61c841ab 100644 --- a/DecSm.Atom.Tests/ClassTests/Args/CommandLineArgsTests.cs +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Args/CommandLineArgsTests.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.ClassTests.Args; +namespace Invex.Atom.Build.Tests.ClassTests.Args; [TestFixture] -public class CommandLineArgsTests +internal sealed class CommandLineArgsTests { [Test] public void HasHelp_WhenHelpArgIsPresent_ShouldReturnTrue() @@ -27,7 +27,7 @@ public void HasHelp_WhenHelpArgIsNotPresent_ShouldReturnFalse() var args = new CommandLineArgs(true, new List { - new GenArg(), + new ParamArg("param-1", "Param1", "1"), }); // Act @@ -37,40 +37,6 @@ public void HasHelp_WhenHelpArgIsNotPresent_ShouldReturnFalse() result.ShouldBeFalse(); } - [Test] - public void HasGen_WhenGenArgIsPresent_ShouldReturnTrue() - { - // Arrange - var args = new CommandLineArgs(true, - new List - { - new GenArg(), - }); - - // Act - var result = args.HasGen; - - // Assert - result.ShouldBeTrue(); - } - - [Test] - public void HasGen_WhenGenArgIsNotPresent_ShouldReturnFalse() - { - // Arrange - var args = new CommandLineArgs(true, - new List - { - new HelpArg(), - }); - - // Act - var result = args.HasGen; - - // Assert - result.ShouldBeFalse(); - } - [Test] public void HasSkip_WhenSkipArgIsPresent_ShouldReturnTrue() { @@ -243,4 +209,82 @@ public void Commands_ShouldReturnAllCommandArgs() () => result[1] .ShouldBeEquivalentTo(commandArg2)); } + + [Test] + public void HasInteractive_WhenInteractiveArgIsPresent_ShouldReturnTrue() + { + var args = new CommandLineArgs(true, [new InteractiveArg()]); + args.HasInteractive.ShouldBeTrue(); + } + + [Test] + public void HasInteractive_WhenInteractiveArgIsNotPresent_ShouldReturnFalse() + { + var args = new CommandLineArgs(true, [new HelpArg()]); + args.HasInteractive.ShouldBeFalse(); + } + + [Test] + public void HasProject_WhenProjectArgIsNotPresent_ShouldReturnFalse() + { + var args = new CommandLineArgs(true, [new HelpArg()]); + args.HasProject.ShouldBeFalse(); + } + + [Test] + public void ProjectName_WhenProjectArgIsPresent_ReturnsProjectName() + { + var args = new CommandLineArgs(true, [new ProjectArg("MyProject")]); + args.ProjectName.ShouldBe("MyProject"); + } + + [Test] + public void ProjectName_WhenProjectArgIsNotPresent_ReturnsDefaultAtom() + { + var args = new CommandLineArgs(true, []); + args.ProjectName.ShouldBe("_atom"); + } + + [Test] + public void GetValidationErrors_WhenIsValidFalse_ContainsParseError() + { + var args = new CommandLineArgs(false, []); + var errors = args.GetValidationErrors(); + errors.ShouldContain(e => e.Contains("could not be parsed")); + } + + [Test] + public void GetValidationErrors_WhenCommandHasEmptyName_ContainsError() + { + var args = new CommandLineArgs(true, [new CommandArg("")]); + var errors = args.GetValidationErrors(); + errors.ShouldContain(e => e.Contains("Target name cannot be empty")); + } + + [Test] + public void GetValidationErrors_WhenParamHasEmptyName_ContainsError() + { + var args = new CommandLineArgs(true, [new ParamArg("", "", "someValue")]); + var errors = args.GetValidationErrors(); + errors.ShouldContain(e => e.Contains("Parameter name cannot be empty")); + } + + [Test] + public void GetValidationErrors_WhenArgsAreValid_ReturnsEmpty() + { + var args = new CommandLineArgs(true, [new CommandArg("Build"), new ParamArg("version", "Version", "1.0.0")]); + + args + .GetValidationErrors() + .ShouldBeEmpty(); + } + + [Test] + public void GetValidationErrors_WhenMultipleErrors_ReturnsAll() + { + var args = new CommandLineArgs(false, [new CommandArg(""), new CommandArg(" "), new ParamArg("", "", "val")]); + + var errors = args.GetValidationErrors(); + errors.Count.ShouldBeGreaterThanOrEqualTo(3); + } } diff --git a/DecSm.Atom.Tests/ClassTests/Build/BuildExecutorTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Build/BuildExecutorTests.cs similarity index 50% rename from DecSm.Atom.Tests/ClassTests/Build/BuildExecutorTests.cs rename to tests/Invex.Atom.Build.Tests/ClassTests/Build/BuildExecutorTests.cs index ad7d8b8f..549a34f9 100644 --- a/DecSm.Atom.Tests/ClassTests/Build/BuildExecutorTests.cs +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Build/BuildExecutorTests.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.ClassTests.Build; +namespace Invex.Atom.Build.Tests.ClassTests.Build; [TestFixture] -public class BuildExecutorTests +internal sealed class BuildExecutorTests { [SetUp] public void SetUp() @@ -16,7 +16,7 @@ public void SetUp() }; _paramService = A.Fake(); - _workflowVariableService = A.Fake(); + _variableService = A.Fake(); _outcomeReporters = []; _console = new(); _reportService = new(); @@ -32,7 +32,7 @@ public void TearDown() => private IParamService _paramService; - private IWorkflowVariableService _workflowVariableService; + private IVariableService _variableService; private IReadOnlyList _outcomeReporters; private TestConsole _console; @@ -48,7 +48,7 @@ public async Task Execute_WithNoCommand_SucceedsAndLogs() var buildExecutor = new BuildExecutor(_commandLineArgs, _buildModel, _paramService, - _workflowVariableService, + _variableService, _outcomeReporters, _console, _reportService, @@ -113,7 +113,7 @@ public async Task Execute_WhenBuildIsValid_SucceedsAndLogs() var buildExecutor = new BuildExecutor(_commandLineArgs, _buildModel, _paramService, - _workflowVariableService, + _variableService, _outcomeReporters, _console, _reportService, @@ -125,4 +125,88 @@ public async Task Execute_WhenBuildIsValid_SucceedsAndLogs() // Assert testVal.Value.ShouldBe("Test"); } + + [Test] + public async Task Execute_WithMultipleMissingRequiredParams_ReportsAllMissingParams() + { + // Arrange + var param1 = new ParamModel("Param1") + { + ArgName = "param-1", + Description = "Param 1", + DefaultValue = null, + Sources = ParamSource.All, + IsSecret = false, + ChainedParams = [], + }; + + var param2 = new ParamModel("Param2") + { + ArgName = "param-2", + Description = "Param 2", + DefaultValue = null, + Sources = ParamSource.All, + IsSecret = false, + ChainedParams = [], + }; + + _commandLineArgs = new(true, [new CommandArg("Test")]); + + var target = new TargetModel("Test", null, false) + { + Tasks = [_ => Task.CompletedTask], + Params = [new(param1, true), new(param2, true)], + ConsumedArtifacts = [], + ProducedArtifacts = [], + ConsumedVariables = [], + ProducedVariables = [], + Dependencies = [], + DeclaringAssembly = Assembly.GetExecutingAssembly(), + }; + + _buildModel = new() + { + Targets = [target], + TargetStates = new Dictionary + { + { + target, new(target.Name) + { + Status = TargetRunState.PendingRun, + } + }, + }, + DeclaringAssembly = Assembly.GetExecutingAssembly(), + }; + + // ParamService returns null/empty for both params + A + .CallTo(() => _paramService.GetParam(A._, A.Ignored)) + .Returns(null); + + A + .CallTo(() => _paramService.CreateNoCacheScope()) + .Returns(new ActionScope()); + + var testLoggerProvider = new TestLoggerProvider(); + var loggerFactory = LoggerFactory.Create(b => b.AddProvider(testLoggerProvider)); + var testLogger = loggerFactory.CreateLogger(); + + var buildExecutor = new BuildExecutor(_commandLineArgs, + _buildModel, + _paramService, + _variableService, + _outcomeReporters, + _console, + _reportService, + testLogger); + + // Act + await Should.ThrowAsync(buildExecutor.Execute(CancellationToken.None)); + + // Assert — both params should be reported in logs + var logOutput = testLoggerProvider.Logger.LogContent.ToString(); + logOutput.ShouldContain("param-1"); + logOutput.ShouldContain("param-2"); + } } diff --git a/DecSm.Atom.Tests/ClassTests/Build/BuildResolverTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Build/BuildResolverTests.cs similarity index 67% rename from DecSm.Atom.Tests/ClassTests/Build/BuildResolverTests.cs rename to tests/Invex.Atom.Build.Tests/ClassTests/Build/BuildResolverTests.cs index f4290958..f362857a 100644 --- a/DecSm.Atom.Tests/ClassTests/Build/BuildResolverTests.cs +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Build/BuildResolverTests.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.ClassTests.Build; +namespace Invex.Atom.Build.Tests.ClassTests.Build; [TestFixture] -public class BuildResolverTests +internal sealed class BuildResolverTests { [SetUp] public void Setup() => @@ -13,7 +13,7 @@ public void Setup() => public void TearDown() => _services.Dispose(); - private class TestBuildDefinition(IServiceProvider services) : MinimalBuildDefinition(services) + private class TestBuildDefinition(IServiceProvider services) : BuildDefinition(services) { public IReadOnlyDictionary ManualTargetDefinitions { get; init; } = new Dictionary(); @@ -94,9 +94,10 @@ public void Resolve_WithCircularDependency_ThrowsException() var logger = A.Fake>(); var buildResolver = new BuildResolver(buildDefinition, paramService, commandLineArgs, logger); - // Act - // Assert - Assert.Throws(() => buildResolver.Resolve()); + // Act & Assert + var ex = Assert.Throws(() => buildResolver.Resolve()); + ex.Message.ShouldContain("Circular dependency detected"); + ex.ReportData.ShouldNotBeNull(); } [Test] @@ -234,9 +235,35 @@ public void Resolve_WithMissingDependency_ThrowsException() var logger = A.Fake>(); var buildResolver = new BuildResolver(buildDefinition, paramService, commandLineArgs, logger); - // Act - // Assert - Assert.Throws(() => buildResolver.Resolve()); + // Act & Assert + var ex = Should.Throw(buildResolver.Resolve); + ex.Message.ShouldContain("depends on target 'Target2' which does not exist"); + } + + [Test] + public void Resolve_WithComplexCircularDependency_ThrowsException() + { + // Arrange - Three node cycle: A -> B -> C -> A + var buildDefinition = new TestBuildDefinition(_services) + { + ManualTargetDefinitions = new Dictionary + { + ["TargetA"] = t => t.DependsOn("TargetB"), + ["TargetB"] = t => t.DependsOn("TargetC"), + ["TargetC"] = t => t.DependsOn("TargetA"), + }, + }; + + var commandLineArgs = new CommandLineArgs(true, []); + var paramService = A.Fake(); + var logger = A.Fake>(); + var buildResolver = new BuildResolver(buildDefinition, paramService, commandLineArgs, logger); + + // Act & Assert + var ex = Should.Throw(buildResolver.Resolve); + ex.Message.ShouldContain("Circular dependency detected"); + ex.ReportData.ShouldNotBeNull(); + ex.ReportData.ShouldBeOfType(); } [Test] @@ -343,4 +370,112 @@ public void Resolve_WithArtifactDependencies_ReturnsModelWithResolvedArtifactDep .Status .ShouldBe(TargetRunState.PendingRun)); } + + [Test] + public void Resolve_WithCircularChainedParams_DoesNotStackOverflow() + { + // Arrange — Param1 chains to Param2, Param2 chains back to Param1 + var buildDefinition = new TestBuildDefinitionWithParams(_services, + new Dictionary + { + ["Param1"] = new("Param1") + { + ArgName = "param-1", + Description = "Param 1", + Sources = ParamSource.All, + IsSecret = false, + ChainedParams = ["Param2"], + }, + ["Param2"] = new("Param2") + { + ArgName = "param-2", + Description = "Param 2", + Sources = ParamSource.All, + IsSecret = false, + ChainedParams = ["Param1"], + }, + }) + { + ManualTargetDefinitions = new Dictionary + { + ["Target1"] = t => t + .RequiresParam("Param1") + .Executes(() => Task.CompletedTask), + }, + }; + + var commandLineArgs = new CommandLineArgs(true, [new CommandArg("Target1")]); + var paramService = A.Fake(); + var logger = A.Fake>(); + var buildResolver = new BuildResolver(buildDefinition, paramService, commandLineArgs, logger); + + // Act — should not throw StackOverflowException + var buildModel = buildResolver.Resolve(); + + // Assert — Target1 should have both params resolved (each appearing once) + var target = buildModel.Targets.First(t => t.Name == "Target1"); + target.Params.Count.ShouldBe(2); + target.Params.ShouldContain(p => p.Param.Name == "Param1"); + target.Params.ShouldContain(p => p.Param.Name == "Param2"); + } + + [Test] + public void Resolve_WithSelfReferencingChainedParam_DoesNotStackOverflow() + { + // Arrange — Param1 chains to itself + var buildDefinition = new TestBuildDefinitionWithParams(_services, + new Dictionary + { + ["Param1"] = new("Param1") + { + ArgName = "param-1", + Description = "Param 1", + Sources = ParamSource.All, + IsSecret = false, + ChainedParams = ["Param1"], + }, + }) + { + ManualTargetDefinitions = new Dictionary + { + ["Target1"] = t => t + .RequiresParam("Param1") + .Executes(() => Task.CompletedTask), + }, + }; + + var commandLineArgs = new CommandLineArgs(true, [new CommandArg("Target1")]); + var paramService = A.Fake(); + var logger = A.Fake>(); + var buildResolver = new BuildResolver(buildDefinition, paramService, commandLineArgs, logger); + + // Act — should not throw StackOverflowException + var buildModel = buildResolver.Resolve(); + + // Assert — Target1 should have Param1 resolved once (not infinite) + var target = buildModel.Targets.First(t => t.Name == "Target1"); + target.Params.ShouldHaveSingleItem(); + + target + .Params[0] + .Param + .Name + .ShouldBe("Param1"); + } + + private class TestBuildDefinitionWithParams( + IServiceProvider services, + IReadOnlyDictionary paramDefinitions + ) : BuildDefinition(services) + { + public IReadOnlyDictionary ManualTargetDefinitions { get; init; } = + new Dictionary(); + + public override IReadOnlyDictionary TargetDefinitions => ManualTargetDefinitions; + + public override IReadOnlyDictionary ParamDefinitions => paramDefinitions; + + public override object? AccessParam(string paramName) => + null; + } } diff --git a/tests/Invex.Atom.Build.Tests/ClassTests/Build/Definition/TargetDefinitionExtensionsTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Build/Definition/TargetDefinitionExtensionsTests.cs new file mode 100644 index 00000000..fb8c7b05 --- /dev/null +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Build/Definition/TargetDefinitionExtensionsTests.cs @@ -0,0 +1,87 @@ +namespace Invex.Atom.Build.Tests.ClassTests.Build.Definition; + +[TestFixture] +[SuppressMessage("ReSharper", "ConvertToLocalFunction")] +internal sealed class TargetDefinitionExtensionsTests +{ + private static TargetDefinition MakeTarget(string name = "Test") => + new() + { + Name = name, + }; + + [Test] + public void DependsOn_TwoTargets_AddsBothDependencies() + { + var t = MakeTarget(); + Target target1 = x => x; + Target target2 = x => x; + + t.DependsOn(target1, target2); + + t.Dependencies.Count.ShouldBe(2); + t.Dependencies.ShouldContain("target1"); + t.Dependencies.ShouldContain("target2"); + } + + [Test] + public void DependsOn_ThreeTargets_AddsAllDependencies() + { + var t = MakeTarget(); + Target alpha = x => x; + Target beta = x => x; + Target gamma = x => x; + + t.DependsOn(alpha, beta, gamma); + + t.Dependencies.Count.ShouldBe(3); + t.Dependencies.ShouldContain("alpha"); + t.Dependencies.ShouldContain("beta"); + t.Dependencies.ShouldContain("gamma"); + } + + [Test] + public void DependsOn_FourTargets_AddsAllDependencies() + { + var t = MakeTarget(); + Target step1 = x => x; + Target step2 = x => x; + Target step3 = x => x; + Target step4 = x => x; + + t.DependsOn(step1, step2, step3, step4); + + t.Dependencies.Count.ShouldBe(4); + t.Dependencies.ShouldContain("step1"); + t.Dependencies.ShouldContain("step4"); + } + + [Test] + public void DependsOn_FiveTargets_AddsAllDependencies() + { + var t = MakeTarget(); + Target job1 = x => x; + Target job2 = x => x; + Target job3 = x => x; + Target job4 = x => x; + Target job5 = x => x; + + t.DependsOn(job1, job2, job3, job4, job5); + + t.Dependencies.Count.ShouldBe(5); + t.Dependencies.ShouldContain("job1"); + t.Dependencies.ShouldContain("job5"); + } + + [Test] + public void DependsOn_TwoTargets_ReturnsSameInstance() + { + var t = MakeTarget(); + Target a = x => x; + Target b = x => x; + + var result = t.DependsOn(a, b); + + result.ShouldBeSameAs(t); + } +} diff --git a/tests/Invex.Atom.Build.Tests/ClassTests/Build/Definition/TargetDefinitionTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Build/Definition/TargetDefinitionTests.cs new file mode 100644 index 00000000..29d93acb --- /dev/null +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Build/Definition/TargetDefinitionTests.cs @@ -0,0 +1,469 @@ +namespace Invex.Atom.Build.Tests.ClassTests.Build.Definition; + +[TestFixture] +internal sealed class TargetDefinitionTests +{ + [Test] + public void WithDescription_SetsDescription() + { + // Arrange + const string description = "description"; + + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + // Act + targetDefinition.DescribedAs(description); + + // Assert + targetDefinition.Description.ShouldBe(description); + } + + [Test] + public void Executes_SetsSingleTask() + { + // Arrange + var task = Task.CompletedTask; + + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + // Act + targetDefinition.Executes(() => task); + + // Assert + targetDefinition.Tasks.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), + x => x.Count.ShouldBe(1), + x => x[0](CancellationToken.None) + .ShouldBe(task)); + } + + [Test] + public void Executes_SetsMultipleTasks() + { + // Arrange + var task1 = Task.CompletedTask; + var task2 = Task.Delay(1); + + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + // Act + targetDefinition + .Executes(() => task1) + .Executes(() => task2); + + // Assert + targetDefinition.Tasks.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), + x => x.Count.ShouldBe(2), + x => x[0](CancellationToken.None) + .ShouldBe(task1), + x => x[1](CancellationToken.None) + .ShouldBe(task2)); + } + + // ReSharper disable once ClassNeverInstantiated.Local + private interface ITestTarget : IBuildDefinition + { + // ReSharper disable once UnusedMember.Local +#pragma warning disable CA1822 + Target TestTarget => x => x; +#pragma warning restore CA1822 + } + + [Test] + public void DependsOn_AddsDependency() + { + // Arrange + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + // Act + targetDefinition.DependsOn(nameof(ITestTarget.TestTarget)); + + // Assert + targetDefinition.Dependencies.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), + x => x.Count.ShouldBe(1), + x => x[0] + .ShouldBe(nameof(ITestTarget.TestTarget))); + } + + [Test] + public void RequiresParam_AddsRequiredParam() + { + // Arrange + const string paramName = "ParamName"; + + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + // Act + targetDefinition.RequiresParam(paramName); + + // Assert + targetDefinition.Params.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), + x => x.Count.ShouldBe(1), + x => x[0] + .Param + .ShouldBe(paramName)); + } + + [Test] + public void ProducesArtifact_AddsProducedArtifact() + { + // Arrange + const string artifactName = "ArtifactName"; + + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + // Act + targetDefinition.ProducesArtifact(artifactName); + + // Assert + targetDefinition.ProducedArtifacts.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), + x => x.Count.ShouldBe(1), + x => x[0] + .ArtifactName + .ShouldBe(artifactName)); + } + + [Test] + public void ConsumesArtifact_AddsConsumedArtifact() + { + // Arrange + const string artifactName = "ArtifactName"; + + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + // Act + targetDefinition.ConsumesArtifact(nameof(ITestTarget.TestTarget), artifactName); + + // Assert + targetDefinition.ConsumedArtifacts.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), + x => x.Count.ShouldBe(1), + x => x[0] + .ArtifactName + .ShouldBe(artifactName), + x => x[0] + .TargetName + .ShouldBe(nameof(ITestTarget.TestTarget))); + } + + [Test] + public void ProducesVariable_AddsProducedVariable() + { + // Arrange + const string variableName = "VariableName"; + + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + // Act + targetDefinition.ProducesVariable(variableName); + + // Assert + targetDefinition.ProducedVariables.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), + x => x.Count.ShouldBe(1), + x => x[0] + .ShouldBe(variableName)); + } + + [Test] + public void ConsumesVariable_AddsConsumedVariable() + { + // Arrange + const string variableName = "VariableName"; + + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + // Act + targetDefinition.ConsumesVariable(nameof(ITestTarget.TestTarget), variableName); + + // Assert + targetDefinition.ConsumedVariables.ShouldSatisfyAllConditions(x => x.ShouldNotBeEmpty(), + x => x.Count.ShouldBe(1), + x => x[0] + .VariableName + .ShouldBe(variableName), + x => x[0] + .TargetName + .ShouldBe(nameof(ITestTarget.TestTarget))); + } + + [Test] + public void IsHidden_DefaultIsFalse() + { + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.Hidden.ShouldBeFalse(); + } + + [Test] + public void IsHidden_SetsHiddenTrue() + { + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.IsHidden(); + targetDefinition.Hidden.ShouldBeTrue(); + } + + [Test] + public void IsHidden_ExplicitFalse_SetsHiddenFalse() + { + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.IsHidden(false); + targetDefinition.Hidden.ShouldBeFalse(); + } + + [Test] + public void UsesParam_AddsOptionalParam() + { + const string paramName = "ParamName"; + + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.UsesParam(paramName); + + targetDefinition.Params.ShouldSatisfyAllConditions(x => x.Count.ShouldBe(1), + x => x[0] + .Param + .ShouldBe(paramName), + x => x[0] + .Required + .ShouldBeFalse()); + } + + [Test] + public void UsesParam_MultipleParams_AddsAll() + { + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.UsesParam("Param1", "Param2"); + targetDefinition.Params.Count.ShouldBe(2); + targetDefinition.Params.ShouldAllBe(p => !p.Required); + } + + [Test] + public void RequiresParam_SetsRequired() + { + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.RequiresParam("RequiredParam"); + + targetDefinition + .Params + .Single() + .Required + .ShouldBeTrue(); + } + + [Test] + public async Task Executes_SynchronousAction_IsInvoked() + { + var called = false; + + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.Executes(() => called = true); + + await targetDefinition + .Tasks + .Single()(CancellationToken.None); + + called.ShouldBeTrue(); + } + + [Test] + public async Task Executes_FuncWithCancellationToken_PassesToken() + { + var capturedToken = CancellationToken.None; + using var cts = new CancellationTokenSource(); + + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.Executes(ct => + { + capturedToken = ct; + + return Task.CompletedTask; + }); + + await targetDefinition + .Tasks + .Single()(cts.Token); + + capturedToken.ShouldBe(cts.Token); + } + + [Test] + public void DependsOn_NullOrWhiteSpaceString_ThrowsArgumentException() + { + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + Should.Throw(() => targetDefinition.DependsOn((string)null!)); + Should.Throw(() => targetDefinition.DependsOn(" ")); + } + + [Test] + public void DependsOn_TargetDefinitionOverload_AddsByName() + { + var dep = new TargetDefinition + { + Name = "DepTarget", + }; + + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.DependsOn(dep); + + targetDefinition.Dependencies.ShouldHaveSingleItem(); + + targetDefinition + .Dependencies[0] + .ShouldBe("DepTarget"); + } + + [Test] + public void ProducesArtifacts_AddsMultipleArtifacts() + { + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.ProducesArtifacts(["Art1", "Art2"]); + + targetDefinition.ProducedArtifacts.Count.ShouldBe(2); + + targetDefinition + .ProducedArtifacts + .Select(a => a.ArtifactName) + .ShouldBe(["Art1", "Art2"]); + } + + [Test] + public void ProducesArtifact_WithBuildSlice_SetsBuildSlice() + { + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.ProducesArtifact("Art1", "linux-x64"); + + targetDefinition + .ProducedArtifacts[0] + .BuildSlice + .ShouldBe("linux-x64"); + } + + [Test] + public void ConsumesArtifacts_MultipleNames_AddsAll() + { + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.ConsumesArtifacts("Producer", ["Artifact1", "Artifact2"]); + + targetDefinition.ConsumedArtifacts.Count.ShouldBe(2); + targetDefinition.ConsumedArtifacts.ShouldAllBe(a => a.TargetName == "Producer"); + } + + [Test] + public void ConsumesArtifact_MultipleSlices_AddsOnePerSlice() + { + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.ConsumesArtifact("Producer", "Artifact1", ["linux-x64", "win-x64"]); + + targetDefinition.ConsumedArtifacts.Count.ShouldBe(2); + + targetDefinition + .ConsumedArtifacts + .Select(a => a.BuildSlice) + .ShouldBe(["linux-x64", "win-x64"]); + } + + [Test] + public void ConsumesArtifacts_NamesAndSlices_AddsCrossProduct() + { + var targetDefinition = new TargetDefinition + { + Name = "name", + }; + + targetDefinition.ConsumesArtifacts("Producer", ["Art1", "Art2"], ["linux-x64", "win-x64"]); + + // 2 artifacts × 2 slices = 4 entries + targetDefinition.ConsumedArtifacts.Count.ShouldBe(4); + + targetDefinition + .ConsumedArtifacts + .Select(a => a.ArtifactName) + .Distinct() + .ShouldBe(["Art1", "Art2"], true); + + targetDefinition + .ConsumedArtifacts + .Select(a => a.BuildSlice) + .Distinct() + .ShouldBe(["linux-x64", "win-x64"], true); + } +} diff --git a/DecSm.Atom.Tests/ClassTests/Build/Model/BuildModelTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Build/Model/BuildModelTests.cs similarity index 74% rename from DecSm.Atom.Tests/ClassTests/Build/Model/BuildModelTests.cs rename to tests/Invex.Atom.Build.Tests/ClassTests/Build/Model/BuildModelTests.cs index 6c19c4b8..2d25c1b3 100644 --- a/DecSm.Atom.Tests/ClassTests/Build/Model/BuildModelTests.cs +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Build/Model/BuildModelTests.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Tests.ClassTests.Build.Model; +namespace Invex.Atom.Build.Tests.ClassTests.Build.Model; [TestFixture] -public class BuildModelTests +internal sealed class BuildModelTests { [Test] public void CurrentTarget_WhenNoTargets_ReturnsNull() @@ -188,4 +188,67 @@ void Act() // Assert Assert.Throws(Act); } + + [Test] + public void GetTargetState_WhenTargetExists_ReturnsState() + { + // Arrange + var targetModel = TestTargetModel; + + var targetState = new TargetState(targetModel.Name) + { + Status = TargetRunState.Succeeded, + }; + + var buildModel = new BuildModel + { + Targets = new List + { + targetModel, + }, + TargetStates = new Dictionary + { + { targetModel, targetState }, + }, + DeclaringAssembly = Assembly.GetExecutingAssembly(), + }; + + // Act + var state = buildModel.GetTargetState(targetModel); + + // Assert + state.ShouldBe(targetState); + } + + [Test] + [SuppressMessage("ReSharper", "MoveLocalFunctionAfterJumpStatement")] + public void GetTargetState_WhenTargetNotInStates_ThrowsInvalidOperationException() + { + // Arrange + var targetModel = TestTargetModel; + + var otherTarget = TestTargetModel with + { + Name = "OtherTarget", + }; + + var buildModel = new BuildModel + { + Targets = new List + { + targetModel, + }, + TargetStates = new Dictionary(), + DeclaringAssembly = Assembly.GetExecutingAssembly(), + }; + + // Act + void Act() + { + buildModel.GetTargetState(otherTarget); + } + + // Assert + Assert.Throws(Act); + } } diff --git a/DecSm.Atom.Tests/ClassTests/Hosting/HostExtensions.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Hosting/HostExtensions.cs similarity index 86% rename from DecSm.Atom.Tests/ClassTests/Hosting/HostExtensions.cs rename to tests/Invex.Atom.Build.Tests/ClassTests/Hosting/HostExtensions.cs index 66daae27..532840f5 100644 --- a/DecSm.Atom.Tests/ClassTests/Hosting/HostExtensions.cs +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Hosting/HostExtensions.cs @@ -1,11 +1,15 @@ -namespace DecSm.Atom.Tests.ClassTests.Hosting; +using Invex.Atom.Build.BuildOptions; + +namespace Invex.Atom.Build.Tests.ClassTests.Hosting; [TestFixture] -public class HostExtensionsTests +internal sealed class HostExtensionsTests { [UsedImplicitly] - private class TestBuildDefinition(IServiceProvider services) : MinimalBuildDefinition(services), IBuildDefinition + private class TestBuildDefinition(IServiceProvider services) : BuildDefinition(services), IBuildDefinition { + public IReadOnlyList Options => []; + public override IReadOnlyDictionary TargetDefinitions => new Dictionary(); public override IReadOnlyDictionary ParamDefinitions => @@ -46,7 +50,7 @@ public void AddAtom_RegistersRequiredServices() .ShouldNotBeNull(); serviceProvider - .GetService() + .GetService() .ShouldNotBeNull(); serviceProvider @@ -61,10 +65,6 @@ public void AddAtom_RegistersRequiredServices() .GetService() .ShouldNotBeNull(); - serviceProvider - .GetService() - .ShouldNotBeNull(); - serviceProvider .GetService() .ShouldNotBeNull(); @@ -74,11 +74,11 @@ public void AddAtom_RegistersRequiredServices() .ShouldNotBeNull(); serviceProvider - .GetService() + .GetService() .ShouldNotBeNull(); serviceProvider - .GetService() + .GetService() .ShouldNotBeNull(); serviceProvider @@ -105,10 +105,6 @@ public void AddAtom_RegistersRequiredServices() .GetService() .ShouldNotBeNull(); - serviceProvider - .GetService() - .ShouldNotBeNull(); - serviceProvider .GetService() .ShouldNotBeNull(); diff --git a/DecSm.Atom.Tests/ClassTests/Logging/MaskingAnsiConsoleOutputTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Logging/MaskingAnsiConsoleOutputTests.cs similarity index 69% rename from DecSm.Atom.Tests/ClassTests/Logging/MaskingAnsiConsoleOutputTests.cs rename to tests/Invex.Atom.Build.Tests/ClassTests/Logging/MaskingAnsiConsoleOutputTests.cs index 3996ecae..f3fdeafe 100644 --- a/DecSm.Atom.Tests/ClassTests/Logging/MaskingAnsiConsoleOutputTests.cs +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Logging/MaskingAnsiConsoleOutputTests.cs @@ -1,21 +1,25 @@ -namespace DecSm.Atom.Tests.ClassTests.Logging; +namespace Invex.Atom.Build.Tests.ClassTests.Logging; [TestFixture] -public class MaskingAnsiConsoleOutputTests +internal sealed class MaskingAnsiConsoleOutputTests { [SetUp] public void SetUp() { - // Install a stub param service that masks our known secret - ServiceStaticAccessor.Service = new StubParamService(Secret, MaskedSecret); - _writer = new(new StringBuilder()); // Create a thin output that writes to our StringWriter var rawOutput = new TestAnsiConsoleOutput(_writer); + // Create a stub param service that masks our known secret + var paramService = new StubParamService(Secret, MaskedSecret); + + var serviceProvider = new ServiceCollection() + .AddSingleton(paramService) + .BuildServiceProvider(); + // Wrap it with our masking output - var maskingOutput = new MaskingAnsiConsoleOutput(rawOutput); + var maskingOutput = new MaskingAnsiConsoleOutput(rawOutput, serviceProvider); var settings = new AnsiConsoleSettings { @@ -26,11 +30,8 @@ public void SetUp() } [TearDown] - public void TearDown() - { - ServiceStaticAccessor.Service = null; + public void TearDown() => _writer.Dispose(); - } private const string Secret = "SuperSecretValue123"; private const string MaskedSecret = "*****"; @@ -91,6 +92,53 @@ public void Exception_Output_ShouldMaskSecrets() TestContext.Out.WriteLine(output); } + [Test] + public async Task WriteAsync_WithSecret_ShouldMaskSecrets() + { + // Arrange — write directly through the MaskingTextWriter via the console's output writer + var writer = _console.Profile.Out.Writer; + + // Act + await writer.WriteAsync($"Token is {Secret} here"); + await writer.FlushAsync(); + var output = _writer.ToString(); + + // Assert + output.ShouldNotContain(Secret); + output.ShouldContain(MaskedSecret); + await TestContext.Out.WriteLineAsync(output); + } + + [Test] + public async Task WriteAsync_WithNull_ShouldNotThrow() + { + // Arrange + var writer = _console.Profile.Out.Writer; + + // Act + await writer.WriteAsync((string?)null); + await writer.FlushAsync(); + var output = _writer.ToString(); + + // Assert + output.ShouldBeEmpty(); + } + + [Test] + public async Task WriteAsync_WithEmptyString_ShouldNotThrow() + { + // Arrange + var writer = _console.Profile.Out.Writer; + + // Act + await writer.WriteAsync(string.Empty); + await writer.FlushAsync(); + var output = _writer.ToString(); + + // Assert + output.ShouldBeEmpty(); + } + private sealed class StubParamService(string secret, string mask) : IParamService { public IDisposable CreateNoCacheScope() => diff --git a/DecSm.Atom.Tests/ClassTests/Logging/ReportLoggerTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Logging/ReportLoggerTests.cs similarity index 76% rename from DecSm.Atom.Tests/ClassTests/Logging/ReportLoggerTests.cs rename to tests/Invex.Atom.Build.Tests/ClassTests/Logging/ReportLoggerTests.cs index f36c4060..3f486471 100644 --- a/DecSm.Atom.Tests/ClassTests/Logging/ReportLoggerTests.cs +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Logging/ReportLoggerTests.cs @@ -1,17 +1,32 @@ -namespace DecSm.Atom.Tests.ClassTests.Logging; +namespace Invex.Atom.Build.Tests.ClassTests.Logging; [TestFixture] -public class ReportLoggerTests +internal sealed class ReportLoggerTests { [SetUp] public void Setup() { _scopeProvider = new(); - _logger = new(_scopeProvider); + _paramService = A.Fake(); + _reportService = new(); + + _serviceProvider = new ServiceCollection() + .AddSingleton(_paramService) + .AddSingleton(_reportService) + .BuildServiceProvider(); + + _logger = new(_serviceProvider, _scopeProvider); } + [TearDown] + public void TearDown() => + _serviceProvider.Dispose(); + private ReportLogger _logger; + private IParamService _paramService; + private ReportService _reportService; private TestScopeProvider _scopeProvider; + private ServiceProvider _serviceProvider; [Test] public void IsEnabled_WhenLogLevelIsNone_ReturnsFalse() @@ -51,14 +66,12 @@ public void Log_WhenLogLevelIsNotEnabled_DoesNotLog() var eventId = new EventId(1, "TestEvent"); const string state = "TestState"; Exception? exception = null; - ServiceStaticAccessor.Service = new(); // Act _logger.Log(logLevel, eventId, state, exception, (s, _) => s); // Assert - ServiceStaticAccessor - .Service + _reportService .GetReportData() .ShouldBeEmpty(); } @@ -71,14 +84,12 @@ public void Log_WhenLogLevelIsEnabled_LogsMessage() var eventId = new EventId(1, "TestEvent"); const string state = "TestState"; Exception? exception = null; - ServiceStaticAccessor.Service = new(); // Act _logger.Log(logLevel, eventId, state, exception, (s, _) => s); // Assert - ServiceStaticAccessor - .Service + _reportService .GetReportData() .ShouldNotBeEmpty(); } diff --git a/DecSm.Atom.Tests/ClassTests/Logging/SpectreLoggerTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Logging/SpectreLoggerTests.cs similarity index 79% rename from DecSm.Atom.Tests/ClassTests/Logging/SpectreLoggerTests.cs rename to tests/Invex.Atom.Build.Tests/ClassTests/Logging/SpectreLoggerTests.cs index c5144f14..815ba729 100644 --- a/DecSm.Atom.Tests/ClassTests/Logging/SpectreLoggerTests.cs +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Logging/SpectreLoggerTests.cs @@ -1,17 +1,30 @@ -namespace DecSm.Atom.Tests.ClassTests.Logging; +namespace Invex.Atom.Build.Tests.ClassTests.Logging; [TestFixture] -public class SpectreLoggerTests +internal sealed class SpectreLoggerTests { [SetUp] public void Setup() { _scopeProvider = new(); - _logger = new("TestCategory", _scopeProvider); + _paramService = A.Fake(); + + _serviceProvider = new ServiceCollection() + .AddSingleton() + .AddSingleton(_paramService) + .BuildServiceProvider(); + + _logger = new("TestCategory", _serviceProvider, _scopeProvider); } + [TearDown] + public void TearDown() => + _serviceProvider.Dispose(); + private SpectreLogger _logger; + private IParamService _paramService; private TestScopeProvider _scopeProvider; + private ServiceProvider _serviceProvider; [Test] public void IsEnabled_WhenLogLevelIsNone_ReturnsFalse() diff --git a/DecSm.Atom.Tests/ClassTests/Params/ParamServiceTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Params/ParamServiceTests.cs similarity index 76% rename from DecSm.Atom.Tests/ClassTests/Params/ParamServiceTests.cs rename to tests/Invex.Atom.Build.Tests/ClassTests/Params/ParamServiceTests.cs index 084a1790..70e9c7d7 100644 --- a/DecSm.Atom.Tests/ClassTests/Params/ParamServiceTests.cs +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Params/ParamServiceTests.cs @@ -1,8 +1,8 @@ -namespace DecSm.Atom.Tests.ClassTests.Params; +namespace Invex.Atom.Build.Tests.ClassTests.Params; [TestFixture] [NonParallelizable] -public class ParamServiceTests +internal sealed class ParamServiceTests { [SetUp] public void Setup() @@ -51,7 +51,7 @@ public void GetParam_WithExpression_ReturnsExpectedValue() _config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - { "Params:test-param", "ConfigValue" }, + ["Params:test-param"] = "ConfigValue", }) .Build(); @@ -87,7 +87,7 @@ public void GetParam_WithString_ReturnsExpectedValue() _config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - { "Params:test-param", "ConfigValue" }, + ["Params:test-param"] = "ConfigValue", }) .Build(); @@ -123,7 +123,7 @@ public void GetParam_WithEnvironmentVariable_ReturnsExpectedValue() _config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - { "Params:test-param", "ConfigValue" }, + ["Params:test-param"] = "ConfigValue", }) .Build(); @@ -355,7 +355,7 @@ public void GetParam_WithNoneFilter_IncludesNone() _config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - { "Params:test-param", "ConfigValue" }, + ["Params:test-param"] = "ConfigValue", }) .Build(); @@ -396,7 +396,7 @@ public void GetParam_WithCommandLineArgsFilter_IncludesCommandLineArgs() _config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - { "Params:test-param", "ConfigValue" }, + ["Params:test-param"] = "ConfigValue", }) .Build(); @@ -437,7 +437,7 @@ public void GetParam_WithEnvironmentVariablesFilter_IncludesEnvironmentVariables _config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - { "Params:test-param", "ConfigValue" }, + ["Params:test-param"] = "ConfigValue", }) .Build(); @@ -478,7 +478,7 @@ public void GetParam_WithConfigurationFilter_IncludesConfiguration() _config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - { "Params:test-param", "ConfigValue" }, + ["Params:test-param"] = "ConfigValue", }) .Build(); @@ -519,7 +519,7 @@ public void GetParam_WithVaultFilter_IncludesVault() _config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - { "Params:test-param", "ConfigValue" }, + ["Params:test-param"] = "ConfigValue", }) .Build(); @@ -560,7 +560,7 @@ public void GetParam_WithSecretsFilter_IncludesVault() _config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - { "Params:test-param", "ConfigValue" }, + ["Params:test-param"] = "ConfigValue", }) .Build(); @@ -639,9 +639,158 @@ public void GetParam_NullableBool_WithDefault_NoMatch_ReturnsDefault(bool? defau result.ShouldBe(defaultValue); } + [Test] + public void CreateOverrideSourcesScope_WhenNested_RestoresPreviousState() + { + // Arrange + var paramDefinition = new ParamDefinition("TestParam") + { + ArgName = "test-param", + Description = "Test parameter", + Sources = ParamSource.All, + IsSecret = false, + ChainedParams = [], + }; + + _args = new(true, [new ParamArg("test-param", "TestParam", "ArgValue")]); + Environment.SetEnvironmentVariable("test-param", "EnvValue"); + + _config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Params:test-param"] = "ConfigValue", + }) + .Build(); + + _paramService = new(_buildDefinition, _args, _console, _config, _logger, _vaultProviders); + + A + .CallTo(() => _buildDefinition.ParamDefinitions) + .Returns(new Dictionary + { + { "TestParam", paramDefinition }, + }); + + // Use no-cache scope to ensure each GetParam call resolves from sources instead of cache + using (_paramService.CreateNoCacheScope()) + { + // Act & Assert + // Without override scope, should use CommandLineArgs (highest priority) + var resultNoScope = _paramService.GetParam("TestParam", "DefaultValue"); + resultNoScope.ShouldBe("ArgValue"); + + using (_paramService.CreateOverrideSourcesScope(ParamSource.Configuration)) + { + // In outer scope, should use Configuration only + var resultOuterScope = _paramService.GetParam("TestParam", "DefaultValue"); + resultOuterScope.ShouldBe("ConfigValue"); + + using (_paramService.CreateOverrideSourcesScope(ParamSource.EnvironmentVariables)) + { + // In inner scope, should use EnvironmentVariables only + var resultInnerScope = _paramService.GetParam("TestParam", "DefaultValue"); + resultInnerScope.ShouldBe("EnvValue"); + } + + // After inner scope disposed, should be back to Configuration + var resultAfterInnerScope = _paramService.GetParam("TestParam", "DefaultValue"); + resultAfterInnerScope.ShouldBe("ConfigValue"); + } + + // After all scopes disposed, should be back to CommandLineArgs + var resultAfterAllScopes = _paramService.GetParam("TestParam", "DefaultValue"); + resultAfterAllScopes.ShouldBe("ArgValue"); + } + } + private class TestSecretsProvider : ISecretsProvider { public string GetSecret(string secretName) => "VaultValue"; } + + [Test] + public void GetParam_WithDefaultValuesOnlyScope_ReturnsDefaultValue() + { + // Arrange + var paramDefinition = new ParamDefinition("TestParam") + { + ArgName = "test-param", + Description = "Test parameter", + Sources = ParamSource.All, + IsSecret = false, + ChainedParams = [], + }; + + _args = new(true, [new ParamArg("test-param", "TestParam", "ArgValue")]); + + _config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Params:test-param"] = "ConfigValue", + }) + .Build(); + + _paramService = new(_buildDefinition, _args, _console, _config, _logger, _vaultProviders); + + A + .CallTo(() => _buildDefinition.ParamDefinitions) + .Returns(new Dictionary + { + { "TestParam", paramDefinition }, + }); + + // Act - inside scope, should return only the default + string? insideScope; + string? outsideScope; + + using (_paramService.CreateDefaultValuesOnlyScope()) + insideScope = _paramService.GetParam("TestParam", "MyDefault"); + + // After scope, should resolve from args (ArgValue) + using (_paramService.CreateNoCacheScope()) + outsideScope = _paramService.GetParam("TestParam", "MyDefault"); + + // Assert + insideScope.ShouldBe("MyDefault"); + outsideScope.ShouldBe("ArgValue"); + } + + [Test] + public void GetParam_WithUnknownParamName_ThrowsInvalidOperationException() + { + A + .CallTo(() => _buildDefinition.ParamDefinitions) + .Returns(new Dictionary()); + + Should.Throw(() => _paramService.GetParam("NonExistent", "default")); + } + + [Test] + public void GetParam_IntType_FromCommandLineArgs_ConvertsCorrectly() + { + var paramDefinition = new ParamDefinition("CountParam") + { + ArgName = "count", + Description = "Count parameter", + Sources = ParamSource.CommandLineArgs, + IsSecret = false, + ChainedParams = [], + }; + + _args = new(true, [new ParamArg("count", "CountParam", "42")]); + + _config = new ConfigurationBuilder().Build(); + _paramService = new(_buildDefinition, _args, _console, _config, _logger, _vaultProviders); + + A + .CallTo(() => _buildDefinition.ParamDefinitions) + .Returns(new Dictionary + { + { "CountParam", paramDefinition }, + }); + + var result = _paramService.GetParam("CountParam", 0); + result.ShouldBe(42); + } } diff --git a/tests/Invex.Atom.Build.Tests/ClassTests/Util/StringUtilTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Util/StringUtilTests.cs new file mode 100644 index 00000000..397181ae --- /dev/null +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Util/StringUtilTests.cs @@ -0,0 +1,201 @@ +namespace Invex.Atom.Build.Tests.ClassTests.Util; + +[TestFixture] +internal sealed class StringUtilTests +{ + [Test] + public void GetLevenshteinDistance_BothNullOrEmpty_ReturnsZero() + { + const string? a = null; + const string? b = null; + + a + .GetLevenshteinDistance(b) + .ShouldBe(0); + } + + [Test] + public void GetLevenshteinDistance_FirstNullOrEmpty_ReturnsLengthOfSecond() + { + const string? a = null; + + a + .GetLevenshteinDistance("abc") + .ShouldBe(3); + } + + [Test] + public void GetLevenshteinDistance_SecondNullOrEmpty_ReturnsLengthOfFirst() => + "abc" + .GetLevenshteinDistance(null) + .ShouldBe(3); + + [Test] + public void GetLevenshteinDistance_IdenticalStrings_ReturnsZero() => + "test" + .GetLevenshteinDistance("test") + .ShouldBe(0); + + [Test] + public void GetLevenshteinDistance_KittenToSitting_ReturnsThree() => + // classic Levenshtein example + "kitten" + .GetLevenshteinDistance("sitting") + .ShouldBe(3); + + [Test] + public void GetLevenshteinDistance_SingleCharDiff_ReturnsOne() => + "abc" + .GetLevenshteinDistance("abd") + .ShouldBe(1); + + [Test] + public void GetLevenshteinDistance_CompletelyDifferent_ReturnsSumOfLengths() => + // "abc" → "xyz": 3 substitutions + "abc" + .GetLevenshteinDistance("xyz") + .ShouldBe(3); + + [Test] + public void GetLevenshteinDistance_IsSymmetric() + { + var d1 = "sunday".GetLevenshteinDistance("saturday"); + var d2 = "saturday".GetLevenshteinDistance("sunday"); + d1.ShouldBe(d2); + } + + [Test] + public void GetLevenshteinDistance_EmptyToNonempty_ReturnsLength() => + "" + .GetLevenshteinDistance("hello") + .ShouldBe(5); + + [Test] + public void SanitizeForLogging_NullInput_ReturnsNull() + { + const string? s = null; + + s + .SanitizeForLogging() + .ShouldBeNull(); + } + + [Test] + public void SanitizeForLogging_EmptyInput_ReturnsEmpty() => + "" + .SanitizeForLogging() + .ShouldBe(""); + + [Test] + public void SanitizeForLogging_NoNewlines_ReturnsSameString() => + "Normal text" + .SanitizeForLogging() + .ShouldBe("Normal text"); + + [Test] + public void SanitizeForLogging_WithNewline_ReplacesWithSpace() => + "Hello\nWorld" + .SanitizeForLogging() + .ShouldBe("Hello World"); + + [Test] + public void SanitizeForLogging_WithCarriageReturn_ReplacesWithSpace() => + "Hello\rWorld" + .SanitizeForLogging() + .ShouldBe("Hello World"); + + [Test] + public void SanitizeForLogging_WithCrLf_ReplacesWithSpace() => + "Hello\r\nWorld" + .SanitizeForLogging() + .ShouldBe("Hello World"); + + [Test] + public void SanitizeForLogging_StripNewlinesFalse_PreservesNewlines() + { + var result = "Hello\nWorld".SanitizeForLogging(false); + result.ShouldBe("Hello\nWorld"); + } + + [Test] + public void SanitizeForLogging_WithinMaxLength_ReturnsEntireString() + { + var s = new string('A', 100); + + s + .SanitizeForLogging(maxLength: 100) + .ShouldBe(s); + } + + [Test] + public void SanitizeForLogging_ExceedsMaxLength_TruncatesWithEllipsis() + { + var s = new string('A', 200); + var result = s.SanitizeForLogging(maxLength: 100); + result.ShouldStartWith("AAA"); + result.Length.ShouldBe(103); // 100 chars + "..." + result.ShouldEndWith("..."); + } + + [Test] + public void SanitizeSecrets_NullInput_ReturnsNull() + { + const string? s = null; + + s + .SanitizeSecrets([]) + .ShouldBeNull(); + } + + [Test] + public void SanitizeSecrets_EmptyInput_ReturnsEmpty() => + "" + .SanitizeSecrets(["secret"]) + .ShouldBe(""); + + [Test] + public void SanitizeSecrets_NoSecrets_ReturnsUnchanged() => + "Hello World" + .SanitizeSecrets([]) + .ShouldBe("Hello World"); + + [Test] + public void SanitizeSecrets_EmptySecretEntries_ReturnsUnchanged() => + "Hello World" + .SanitizeSecrets(["", null!]) + .ShouldBe("Hello World"); + + [Test] + public void SanitizeSecrets_LongSecret_ReplacesWithFiveStars() => + "This is a SecretValue in text." + .SanitizeSecrets(["SecretValue"]) + .ShouldBe("This is a ***** in text."); + + [Test] + public void SanitizeSecrets_ShortSecret_ReplacesWithMatchingStarCount() => + // "abc" has length 3 → 3 stars + "prefix abc suffix" + .SanitizeSecrets(["abc"]) + .ShouldBe("prefix *** suffix"); + + [Test] + public void SanitizeSecrets_MultipleSecrets_ReplacesAll() + { + var result = "alpha is secret1 and beta is secret2".SanitizeSecrets(["secret1", "secret2"]); + result.ShouldNotContain("secret1"); + result.ShouldNotContain("secret2"); + } + + [Test] + public void SanitizeSecrets_CaseInsensitive_ReplacesSecret() => + "SECRETVALUE" + .SanitizeSecrets(["secretvalue"]) + .ShouldBe("*****"); + + [Test] + public void SanitizeSecrets_StringShorterThanAllSecrets_ReturnsUnchanged() => + // "abc" is 3 chars, secret is 10 chars → the input is shorter than any secret + "abc" + .SanitizeSecrets(["longsecret"]) + .ShouldBe("abc"); +} diff --git a/tests/Invex.Atom.Build.Tests/ClassTests/Util/TaskExtensionsTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Util/TaskExtensionsTests.cs new file mode 100644 index 00000000..6542a79c --- /dev/null +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Util/TaskExtensionsTests.cs @@ -0,0 +1,287 @@ +namespace Invex.Atom.Build.Tests.ClassTests.Util; + +[TestFixture] +internal sealed class TaskExtensionsTests +{ + [Test] + public async Task WithRetry_NullTask_ReturnsCompletedTask() + { + // Arrange + Task? task = null; + + // Act & Assert + await task.WithRetry(); + } + + [Test] + public async Task WithRetry_SuccessfulTask_CompletesWithoutException() + { + // Arrange + var task = Task.CompletedTask; + + // Act & Assert + await task.WithRetry(3, TimeSpan.FromMilliseconds(1)); + } + + [Test] + public void WithRetry_NegativeRetryCount_ThrowsArgumentOutOfRangeException() + { + // Arrange + var task = Task.CompletedTask; + + // Act + var ex = Should.Throw(() => task.WithRetry(-1)); + + // Assert + ex.ParamName.ShouldBe("retryCount"); + } + + [Test] + public async Task WithRetry_FaultedTask_WithZeroRetries_ThrowsOriginalException() + { + // Arrange + var original = new InvalidOperationException("boom"); + var task = Task.FromException(original); + + // Act + var ex = await Should.ThrowAsync(async () => + await task.WithRetry(0, TimeSpan.FromMilliseconds(1))); + + // Assert + ex.Message.ShouldBe("boom"); + } + + [Test] + public async Task WithRetry_FaultedTask_WithRetries_ThrowsAggregateWithExpectedInnerCount() + { + // Arrange + var task = Task.FromException(new InvalidOperationException("boom")); + const int retryCount = 2; // total attempts = retryCount + 1 = 3 + + // Act + var aggregate = await Should.ThrowAsync(async () => + await task.WithRetry(retryCount, TimeSpan.FromMilliseconds(1))); + + // Assert + aggregate.InnerExceptions.Count.ShouldBe(retryCount + 1); + + aggregate + .InnerExceptions + .All(e => e is InvalidOperationException) + .ShouldBeTrue(); + } + + [Test] + public async Task WithRetry_CanceledTask_IsNotRetried_ThrowsTaskCanceledImmediately() + { + // Arrange + using var cts = new CancellationTokenSource(); + await cts.CancelAsync(); + var task = Task.FromCanceled(cts.Token); + + // Act + var ex = await Should.ThrowAsync(async () => + await task.WithRetry(3, TimeSpan.FromMilliseconds(25))); + + // Assert + ex.ShouldNotBeNull(); + } + + [Test] + public async Task WithRetry_OperationCanceledException_IsRethrown() + { + // Arrange + var task = Task.Run(() => throw new OperationCanceledException()); + + // Act + var ex = await Should.ThrowAsync(async () => + await task.WithRetry(3, TimeSpan.FromMilliseconds(25))); + + // Assert + ex.ShouldNotBeNull(); + } + + [Test] + public async Task WithRetry_GenericNullTask_ReturnsDefaultValue() + { + Task? task = null; + var result = await task.WithRetry(); + result.ShouldBe(0); + } + + [Test] + public async Task WithRetry_SuccessfulGenericTask_ReturnsResult() + { + var task = Task.FromResult(42); + var result = await task.WithRetry(3, TimeSpan.FromMilliseconds(1)); + result.ShouldBe(42); + } + + [Test] + public void WithRetry_GenericTask_NegativeRetryCount_ThrowsArgumentOutOfRangeException() + { + var task = Task.FromResult(1); + var ex = Should.Throw(() => task.WithRetry(-1)); + ex.ParamName.ShouldBe("retryCount"); + } + + [Test] + public async Task WithRetry_GenericFaultedTask_WithZeroRetries_ThrowsOriginalException() + { + var task = Task.FromException(new InvalidOperationException("oops")); + + var ex = await Should.ThrowAsync(async () => + await task.WithRetry(0, TimeSpan.FromMilliseconds(1))); + + ex.Message.ShouldBe("oops"); + } + + [Test] + public async Task WithRetry_GenericFaultedTask_WithRetries_ThrowsAggregateException() + { + var task = Task.FromException(new InvalidOperationException("oops")); + const int retryCount = 2; + + var aggregate = await Should.ThrowAsync(async () => + await task.WithRetry(retryCount, TimeSpan.FromMilliseconds(1))); + + aggregate.InnerExceptions.Count.ShouldBe(retryCount + 1); + } + + [Test] + public async Task WithRetry_GenericCanceledTask_IsNotRetried() + { + using var cts = new CancellationTokenSource(); + await cts.CancelAsync(); + var task = Task.FromCanceled(cts.Token); + + await Should.ThrowAsync(async () => + await task.WithRetry(3, TimeSpan.FromMilliseconds(25))); + } + + [Test] + public async Task WithRetry_FactorySucceeds_CompletesWithoutException() + { + var callCount = 0; + + var factory = () => + { + callCount++; + + return Task.CompletedTask; + }; + + await factory.WithRetry(3, TimeSpan.FromMilliseconds(1)); + callCount.ShouldBe(1); + } + + [Test] + public async Task WithRetry_FactoryFailsThenSucceeds_RetriesAndSucceeds() + { + var callCount = 0; + + var factory = () => + { + callCount++; + + return callCount < 3 + ? Task.FromException(new InvalidOperationException("fail")) + : Task.CompletedTask; + }; + + await factory.WithRetry(5, TimeSpan.FromMilliseconds(1)); + callCount.ShouldBe(3); + } + + [Test] + public async Task WithRetry_FactoryAlwaysFails_ThrowsAggregateException() + { + var factory = () => Task.FromException(new InvalidOperationException("always fails")); + + var aggregate = + await Should.ThrowAsync(async () => + await factory.WithRetry(2, TimeSpan.FromMilliseconds(1))); + + aggregate.InnerExceptions.Count.ShouldBe(3); // initial + 2 retries + } + + [Test] + public void WithRetry_FactoryNull_ThrowsArgumentNullException() + { + Func? factory = null; + Should.Throw(() => factory!.WithRetry()); + } + + [Test] + public void WithRetry_FactoryNegativeRetryCount_ThrowsArgumentOutOfRangeException() + { + var factory = () => Task.CompletedTask; + Should.Throw(() => factory.WithRetry(-1)); + } + + [Test] + [SuppressMessage("ReSharper", "AccessToDisposedClosure")] + public async Task WithRetry_FactoryCanceled_IsNotRetried() + { + using var cts = new CancellationTokenSource(); + await cts.CancelAsync(); + + var factory = () => Task.FromCanceled(cts.Token); + + await Should.ThrowAsync(async () => + await factory.WithRetry(3, TimeSpan.FromMilliseconds(1), cts.Token)); + } + + [Test] + public async Task WithRetry_GenericFactorySucceeds_ReturnsResult() + { + var factory = () => Task.FromResult(99); + var result = await factory.WithRetry(3, TimeSpan.FromMilliseconds(1)); + result.ShouldBe(99); + } + + [Test] + public async Task WithRetry_GenericFactoryFailsThenSucceeds_RetriesAndReturnsResult() + { + var callCount = 0; + + var factory = () => + { + callCount++; + + return callCount < 3 + ? Task.FromException(new InvalidOperationException("fail")) + : Task.FromResult(7); + }; + + var result = await factory.WithRetry(5, TimeSpan.FromMilliseconds(1)); + result.ShouldBe(7); + callCount.ShouldBe(3); + } + + [Test] + public async Task WithRetry_GenericFactoryAlwaysFails_ThrowsAggregateException() + { + var factory = () => Task.FromException(new InvalidOperationException("always fails")); + + var aggregate = + await Should.ThrowAsync(async () => + await factory.WithRetry(2, TimeSpan.FromMilliseconds(1))); + + aggregate.InnerExceptions.Count.ShouldBe(3); + } + + [Test] + public void WithRetry_GenericFactoryNull_ThrowsArgumentNullException() + { + Func>? factory = null; + Should.Throw(() => factory!.WithRetry()); + } + + [Test] + public void WithRetry_GenericFactoryNegativeRetryCount_ThrowsArgumentOutOfRangeException() + { + var factory = () => Task.FromResult(1); + Should.Throw(() => factory.WithRetry(-1)); + } +} diff --git a/tests/Invex.Atom.Build.Tests/ClassTests/Util/TypeUtilTests.cs b/tests/Invex.Atom.Build.Tests/ClassTests/Util/TypeUtilTests.cs new file mode 100644 index 00000000..487893bc --- /dev/null +++ b/tests/Invex.Atom.Build.Tests/ClassTests/Util/TypeUtilTests.cs @@ -0,0 +1,265 @@ +namespace Invex.Atom.Build.Tests.ClassTests.Util; + +[TestFixture] +internal sealed class TypeUtilTests +{ + [Test] + public void Convert_NullInput_ReturnsDefault() + { + TypeUtil + .Convert(null, null) + .ShouldBeNull(); + + TypeUtil + .Convert(null, null) + .ShouldBe(0); + + TypeUtil + .Convert(null, null) + .ShouldBe(false); + } + + [Test] + public void Convert_WithCustomConverter_UsesConverter() + { + var result = TypeUtil.Convert("99", _ => 42); + result.ShouldBe(42); + } + + [Test] + public void Convert_String_ReturnsSameValue() => + TypeUtil + .Convert("hello", null) + .ShouldBe("hello"); + + [TestCase("true", true)] + [TestCase("True", true)] + [TestCase("TRUE", true)] + [TestCase("false", false)] + [TestCase("False", false)] + public void Convert_Bool_ParsesCorrectly(string input, bool expected) => + TypeUtil + .Convert(input, null) + .ShouldBe(expected); + + [Test] + public void Convert_Bool_InvalidInput_ThrowsException() => + // TryConvert fails, then TypeDescriptor.ConvertFromInvariantString throws + Should.Throw(() => TypeUtil.Convert("notabool", null)); + + [Test] + public void Convert_Char_SingleChar_ReturnsChar() => + TypeUtil + .Convert("A", null) + .ShouldBe('A'); + + [Test] + public void Convert_Char_MultipleChars_ThrowsException() => + // TryConvertSimple returns false for multi-char; TypeDescriptor throws + Should.Throw(() => TypeUtil.Convert("AB", null)); + + [Test] + public void Convert_Int_ParsesCorrectly() => + TypeUtil + .Convert("42", null) + .ShouldBe(42); + + [Test] + public void Convert_Int_InvalidInput_ThrowsException() => + Should.Throw(() => TypeUtil.Convert("notanint", null)); + + [Test] + public void Convert_Long_ParsesCorrectly() => + TypeUtil + .Convert("9999999999", null) + .ShouldBe(9999999999L); + + [Test] + public void Convert_Short_ParsesCorrectly() => + TypeUtil + .Convert("100", null) + .ShouldBe((short)100); + + [Test] + public void Convert_Byte_ParsesCorrectly() => + TypeUtil + .Convert("255", null) + .ShouldBe((byte)255); + + [Test] + public void Convert_SByte_ParsesCorrectly() => + TypeUtil + .Convert("-10", null) + .ShouldBe((sbyte)-10); + + [Test] + public void Convert_UInt_ParsesCorrectly() => + TypeUtil + .Convert("4000000000", null) + .ShouldBe(4000000000u); + + [Test] + public void Convert_ULong_ParsesCorrectly() => + TypeUtil + .Convert("18446744073709551615", null) + .ShouldBe(ulong.MaxValue); + + [Test] + public void Convert_UShort_ParsesCorrectly() => + TypeUtil + .Convert("65535", null) + .ShouldBe(ushort.MaxValue); + + [Test] + public void Convert_Float_ParsesCorrectly() => + TypeUtil + .Convert("3.14", null) + .ShouldBeInRange(3.13f, 3.15f); + + [Test] + public void Convert_Double_ParsesCorrectly() => + TypeUtil + .Convert("2.718281828", null) + .ShouldBeInRange(2.71, 2.72); + + [Test] + public void Convert_Decimal_ParsesCorrectly() => + TypeUtil + .Convert("123.456", null) + .ShouldBe(123.456m); + + [Test] + public void Convert_Guid_ParsesCorrectly() + { + var guid = Guid.NewGuid(); + + TypeUtil + .Convert(guid.ToString(), null) + .ShouldBe(guid); + } + + [Test] + public void Convert_Guid_InvalidInput_ThrowsException() => + Should.Throw(() => TypeUtil.Convert("not-a-guid", null)); + + [Test] + public void Convert_DateTime_ParsesCorrectly() + { + var dt = new DateTime(2024, 6, 15, 0, 0, 0, DateTimeKind.Unspecified); + + TypeUtil + .Convert("2024-06-15", null) + .ShouldBe(dt); + } + + [Test] + public void Convert_TimeSpan_ParsesCorrectly() => + TypeUtil + .Convert("01:30:00", null) + .ShouldBe(TimeSpan.FromMinutes(90)); + + [Test] + public void Convert_DateOnly_ParsesCorrectly() => + TypeUtil + .Convert("2024-06-15", null) + .ShouldBe(new(2024, 6, 15)); + + [Test] + public void Convert_TimeOnly_ParsesCorrectly() => + TypeUtil + .Convert("14:30:00", null) + .ShouldBe(new(14, 30, 0)); + + [Test] + public void Convert_Object_ReturnsSameStringValue() => + TypeUtil + .Convert("something", null) + .ShouldBe("something"); + + [Test] + public void Convert_NullableInt_ParsesCorrectly() => + TypeUtil + .Convert("42", null) + .ShouldBe(42); + + [Test] + public void Convert_NullableBool_ParsesCorrectly() => + TypeUtil + .Convert("true", null) + .ShouldBe(true); + + [Test] + public void Convert_NullableInt_InvalidInput_ThrowsException() => + Should.Throw(() => TypeUtil.Convert("notanint", null)); + + private enum Color + { + Red, + Green, + Blue, + } + + [Test] + public void Convert_Enum_ParsesCorrectly() => + TypeUtil + .Convert("Green", null) + .ShouldBe(Color.Green); + + [Test] + public void Convert_Enum_CaseInsensitive_ParsesCorrectly() => + TypeUtil + .Convert("blue", null) + .ShouldBe(Color.Blue); + + [Test] + public void Convert_Enum_InvalidValue_ThrowsException() => + // Enum.Parse throws; TypeDescriptor also throws for unknown values + Should.Throw(() => TypeUtil.Convert("Purple", null)); + + [Test] + public void Convert_StringArray_ParsesCommaSeparated() + { + var result = TypeUtil.Convert("a,b,c", null); + result.ShouldNotBeNull(); + result.ShouldBe(["a", "b", "c"]); + } + + [Test] + public void Convert_IntArray_ParsesCommaSeparated() + { + var result = TypeUtil.Convert("1,2,3", null); + result.ShouldNotBeNull(); + result.ShouldBe([1, 2, 3]); + } + + [Test] + public void Convert_SingleElementArray_ReturnsSingleElementArray() + { + var result = TypeUtil.Convert("only", null); + result.ShouldNotBeNull(); + result.Length.ShouldBe(1); + + result[0] + .ShouldBe("only"); + } + + [Test] + public void Convert_IReadOnlyListOfString_ParsesCommaSeparated() + { + var result = TypeUtil.Convert>("x,y,z", null); + result.ShouldNotBeNull(); + result.ShouldBe(["x", "y", "z"]); + } + + [Test] + public void Convert_IReadOnlyListOfInt_ParsesCommaSeparated() + { + var result = TypeUtil.Convert>("10,20,30", null); + result.ShouldNotBeNull(); + result.ShouldBe([10, 20, 30]); + } + + [Test] + public void Convert_UnsupportedGenericType_ThrowsNotSupportedException() => + Should.Throw(() => TypeUtil.Convert>("a,b", null)); +} diff --git a/tests/Invex.Atom.Build.Tests/ExceptionTests.cs b/tests/Invex.Atom.Build.Tests/ExceptionTests.cs new file mode 100644 index 00000000..bb9aa784 --- /dev/null +++ b/tests/Invex.Atom.Build.Tests/ExceptionTests.cs @@ -0,0 +1,108 @@ +namespace Invex.Atom.Build.Tests; + +[TestFixture] +internal sealed class ExceptionTests +{ + [Test] + public void AtomException_DefaultConstructor_Creates() + { + var ex = new AtomException(); + ex.ShouldNotBeNull(); + } + + [Test] + public void AtomException_MessageConstructor_SetsMessage() + { + const string message = "Test message"; + var ex = new AtomException(message); + ex.Message.ShouldBe(message); + } + + [Test] + public void AtomException_MessageAndInnerExceptionConstructor_SetsBoth() + { + const string message = "Test message"; + var innerException = new InvalidOperationException("Inner"); + var ex = new AtomException(message, innerException); + ex.Message.ShouldBe(message); + ex.InnerException.ShouldBe(innerException); + } + + [Test] + public void BuildConfigurationException_InheritsFromAtomException() + { + var ex = new BuildConfigurationException("Test"); + ex.ShouldBeAssignableTo(); + ex.ShouldBeAssignableTo(); + } + + [Test] + public void BuildConfigurationException_WithReportData_SetsProperty() + { + var reportData = new ListReportData(["Error 1", "Error 2"]); + + var ex = new BuildConfigurationException("Test") + { + ReportData = reportData, + }; + + ex.ReportData.ShouldBe(reportData); + } + + [Test] + public void BuildConfigurationException_WithoutReportData_PropertyIsNull() + { + var ex = new BuildConfigurationException("Test"); + ex.ReportData.ShouldBeNull(); + } + + [Test] + public void CommandLineException_InheritsFromAtomException() + { + var ex = new CommandLineException("Test"); + ex.ShouldBeAssignableTo(); + ex.ShouldBeAssignableTo(); + } + + [Test] + public void CommandLineException_WithArgumentName_SetsProperty() + { + const string argumentName = "project"; + + var ex = new CommandLineException("Missing value") + { + ArgumentName = argumentName, + }; + + ex.ArgumentName.ShouldBe(argumentName); + } + + [Test] + public void CommandLineException_WithoutArgumentName_PropertyIsNull() + { + var ex = new CommandLineException("Test"); + ex.ArgumentName.ShouldBeNull(); + } + + [Test] + public void StepFailedException_InheritsFromAtomException() + { + var ex = new StepFailedException("Test"); + ex.ShouldBeAssignableTo(); + ex.ShouldBeAssignableTo(); + } + + [Test] + public void AllAtomExceptions_CanBeCaughtAsAtomException() + { + var exceptions = new Exception[] + { + new BuildConfigurationException("Test"), + new CommandLineException("Test"), + new StepFailedException("Test"), + }; + + foreach (var ex in exceptions) + ex.ShouldBeAssignableTo(); + } +} diff --git a/DecSm.Atom.Tests/DecSm.Atom.Tests.csproj b/tests/Invex.Atom.Build.Tests/Invex.Atom.Build.Tests.csproj similarity index 87% rename from DecSm.Atom.Tests/DecSm.Atom.Tests.csproj rename to tests/Invex.Atom.Build.Tests/Invex.Atom.Build.Tests.csproj index 5a92d089..f99ad9b2 100644 --- a/DecSm.Atom.Tests/DecSm.Atom.Tests.csproj +++ b/tests/Invex.Atom.Build.Tests/Invex.Atom.Build.Tests.csproj @@ -3,7 +3,7 @@ net10.0;net9.0;net8.0 false - decsm.atom.tests + invex.atom.tests @@ -17,7 +17,7 @@ - + @@ -50,9 +50,9 @@ - - - + + + diff --git a/DecSm.Atom.Tests/ProjectSourcePath.cs b/tests/Invex.Atom.Build.Tests/ProjectSourcePath.cs similarity index 95% rename from DecSm.Atom.Tests/ProjectSourcePath.cs rename to tests/Invex.Atom.Build.Tests/ProjectSourcePath.cs index 0a83f00a..4c465d8d 100644 --- a/DecSm.Atom.Tests/ProjectSourcePath.cs +++ b/tests/Invex.Atom.Build.Tests/ProjectSourcePath.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Tests; +namespace Invex.Atom.Build.Tests; /// /// Provides the full path to the source directory of the current project.
diff --git a/DecSm.Atom.Tests/Utils/Initializer.cs b/tests/Invex.Atom.Build.Tests/Utils/Initializer.cs similarity index 91% rename from DecSm.Atom.Tests/Utils/Initializer.cs rename to tests/Invex.Atom.Build.Tests/Utils/Initializer.cs index 33772d6c..b7da3318 100644 --- a/DecSm.Atom.Tests/Utils/Initializer.cs +++ b/tests/Invex.Atom.Build.Tests/Utils/Initializer.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.Tests.Utils; +namespace Invex.Atom.Build.Tests.Utils; public static class Initializer { diff --git a/tests/Invex.Atom.Build.Tests/_usings.cs b/tests/Invex.Atom.Build.Tests/_usings.cs new file mode 100644 index 00000000..50fb9e08 --- /dev/null +++ b/tests/Invex.Atom.Build.Tests/_usings.cs @@ -0,0 +1,38 @@ +global using System.Diagnostics; +global using System.Diagnostics.CodeAnalysis; +global using System.IO.Abstractions; +global using System.Linq.Expressions; +global using System.Reflection; +global using System.Runtime.CompilerServices; +global using System.Text; +global using System.Text.Json; +global using Invex.Atom.Build.Args; +global using Invex.Atom.Build.BuildInfo; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.Exceptions; +global using Invex.Atom.Build.FileSystem; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Build.Logging; +global using Invex.Atom.Build.Model; +global using Invex.Atom.Build.Params; +global using Invex.Atom.Build.Reports; +global using Invex.Atom.Build.Secrets; +global using Invex.Atom.Build.Util; +global using Invex.Atom.Build.Util.Scope; +global using Invex.Atom.Build.Variables; +global using Invex.Atom.Build.Tests.BuildTests.Core; +global using Invex.Atom.TestUtils; +global using DiffEngine; +global using FakeItEasy; +global using JetBrains.Annotations; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using NUnit.Framework; +global using Shouldly; +global using Spectre.Console; +global using Spectre.Console.Testing; +global using static Invex.Atom.TestUtils.TestUtils; +global using IHelpService = Invex.Atom.Build.Help.IHelpService; +global using Target = Invex.Atom.Build.Definition.Target; diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/DecSm.Atom.Module.DevopsWorkflows.Tests.csproj b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Invex.Atom.Module.DevopsWorkflows.Tests.csproj similarity index 86% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/DecSm.Atom.Module.DevopsWorkflows.Tests.csproj rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Invex.Atom.Module.DevopsWorkflows.Tests.csproj index fd1c6232..78bbef1b 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/DecSm.Atom.Module.DevopsWorkflows.Tests.csproj +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Invex.Atom.Module.DevopsWorkflows.Tests.csproj @@ -3,7 +3,7 @@ net10.0;net9.0;net8.0 false - decsm.atom.tests + invex.atom.tests @@ -17,7 +17,7 @@ - + @@ -50,9 +50,9 @@ - - - + + + diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/ArtifactBuild.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/ArtifactBuild.cs similarity index 58% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/ArtifactBuild.cs rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/ArtifactBuild.cs index d3c1b557..06a67934 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/ArtifactBuild.cs +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/ArtifactBuild.cs @@ -1,7 +1,8 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Tests.Workflows; +namespace Invex.Atom.Module.DevopsWorkflows.Tests.Workflows; [BuildDefinition] -public partial class ArtifactBuild : MinimalBuildDefinition, +public partial class ArtifactBuild : WorkflowBuildDefinition, + IWorkflowBuildDefinition, IDevopsWorkflows, IArtifactTarget1, IArtifactTarget2, @@ -21,22 +22,28 @@ public partial class ArtifactBuild : MinimalBuildDefinition, ], Targets = [ - WorkflowTargets.ArtifactTarget1, - WorkflowTargets.ArtifactTarget2, - WorkflowTargets.ArtifactTarget3, - WorkflowTargets.ArtifactTarget4.WithMatrixDimensions( - new MatrixDimension(nameof(IArtifactSliceTarget1.Slice)) - { - Values = [IArtifactTarget2.Slice1, IArtifactTarget2.Slice2], - }), + new(nameof(IArtifactTarget1.ArtifactTarget1)), + new(nameof(IArtifactTarget2.ArtifactTarget2)), + new(nameof(IArtifactTarget3.ArtifactTarget3)), + new(nameof(IArtifactTarget4.ArtifactTarget4)) + { + MatrixDimensions = + [ + new(nameof(IArtifactSliceTarget1.TestSlice)) + { + Values = [IArtifactTarget2.Slice1, IArtifactTarget2.Slice2], + }, + ], + }, ], - WorkflowTypes = [Devops.WorkflowType], + Types = [WorkflowTypes.Devops.Pipeline], }, ]; } [BuildDefinition] -public partial class CustomArtifactBuild : MinimalBuildDefinition, +public partial class CustomArtifactBuild : WorkflowBuildDefinition, + IWorkflowBuildDefinition, IDevopsWorkflows, IStoreArtifact, IRetrieveArtifact, @@ -58,18 +65,23 @@ public partial class CustomArtifactBuild : MinimalBuildDefinition, ], Targets = [ - WorkflowTargets.SetupBuildInfo, - WorkflowTargets.ArtifactTarget1, - WorkflowTargets.ArtifactTarget2, - WorkflowTargets.ArtifactTarget3, - WorkflowTargets.ArtifactTarget4.WithMatrixDimensions( - new MatrixDimension(nameof(IArtifactSliceTarget1.Slice)) - { - Values = [IArtifactTarget2.Slice1, IArtifactTarget2.Slice2], - }), + new(nameof(ISetupBuildInfo.SetupBuildInfo)), + new(nameof(IArtifactTarget1.ArtifactTarget1)), + new(nameof(IArtifactTarget2.ArtifactTarget2)), + new(nameof(IArtifactTarget3.ArtifactTarget3)), + new(nameof(IArtifactTarget4.ArtifactTarget4)) + { + MatrixDimensions = + [ + new(nameof(IArtifactSliceTarget1.TestSlice)) + { + Values = [IArtifactTarget2.Slice1, IArtifactTarget2.Slice2], + }, + ], + }, ], - WorkflowTypes = [Devops.WorkflowType], - Options = [UseCustomArtifactProvider.Enabled], + Types = [WorkflowTypes.Devops.Pipeline], + Options = [BuildOptions.Artifacts.UseCustomProvider], }, ]; } @@ -79,8 +91,8 @@ public interface IArtifactSliceTarget1 : IBuildAccessor const string Slice1 = "Slice1"; const string Slice2 = "Slice2"; - [ParamDefinition("slice", "Slice")] - string Slice => GetParam(() => Slice)!; + [ParamDefinition("test-slice", "Test slice")] + string TestSlice => GetParam(() => TestSlice)!; } public interface IArtifactTarget1 diff --git a/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/CheckoutOptionBuild.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/CheckoutOptionBuild.cs new file mode 100644 index 00000000..9d5cf8a2 --- /dev/null +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/CheckoutOptionBuild.cs @@ -0,0 +1,35 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class CheckoutOptionBuild : WorkflowBuildDefinition, + IWorkflowBuildDefinition, + IDevopsWorkflows, + ICheckoutOptionTarget +{ + public override IReadOnlyList Workflows => + [ + new("checkoutoption-workflow") + { + Triggers = [WorkflowTriggers.Manual], + Targets = + [ + new(nameof(ICheckoutOptionTarget.CheckoutOptionTarget)) + { + Options = + [ + BuildOptions.Devops.Steps.Checkout(new() + { + DisplayName = "Checkout with no options", + }), + ], + }, + ], + Types = [WorkflowTypes.Devops.Pipeline], + }, + ]; +} + +public interface ICheckoutOptionTarget +{ + Target CheckoutOptionTarget => t => t; +} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/DependentBuild.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/DependentBuild.cs similarity index 55% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/DependentBuild.cs rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/DependentBuild.cs index 149ca7dd..f34144cb 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/DependentBuild.cs +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/DependentBuild.cs @@ -1,7 +1,11 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; +namespace Invex.Atom.Module.DevopsWorkflows.Tests.Workflows; [BuildDefinition] -public partial class DependentBuild : MinimalBuildDefinition, IGithubWorkflows, IDependentTarget1, IDependentTarget2 +public partial class DependentBuild : WorkflowBuildDefinition, + IWorkflowBuildDefinition, + IDevopsWorkflows, + IDependentTarget1, + IDependentTarget2 { public override IReadOnlyList Workflows => [ @@ -14,8 +18,11 @@ public partial class DependentBuild : MinimalBuildDefinition, IGithubWorkflows, IncludedBranches = ["main"], }, ], - Targets = [WorkflowTargets.DependentTarget1, WorkflowTargets.DependentTarget2], - WorkflowTypes = [new GithubWorkflowType()], + Targets = + [ + new(nameof(IDependentTarget1.DependentTarget1)), new(nameof(IDependentTarget2.DependentTarget2)), + ], + Types = [WorkflowTypes.Devops.Pipeline], }, ]; } diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/DuplicateDependencyBuild.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/DuplicateDependencyBuild.cs similarity index 60% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/DuplicateDependencyBuild.cs rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/DuplicateDependencyBuild.cs index 4ebd7354..7735963f 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/DuplicateDependencyBuild.cs +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/DuplicateDependencyBuild.cs @@ -1,19 +1,22 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; +namespace Invex.Atom.Module.DevopsWorkflows.Tests.Workflows; [BuildDefinition] -public partial class DuplicateDependencyBuild : MinimalBuildDefinition, IGithubWorkflows, IDuplicateDependencyTarget +public partial class DuplicateDependencyBuild : WorkflowBuildDefinition, + IWorkflowBuildDefinition, + IDevopsWorkflows, + IDuplicateDependencyTarget { - public override IReadOnlyList GlobalWorkflowOptions => [UseCustomArtifactProvider.Enabled]; - public override IReadOnlyList Workflows => [ new("duplicatedependency-workflow") { - Triggers = [ManualTrigger.Empty], - Targets = [WorkflowTargets.DuplicateDependencyTarget1], - WorkflowTypes = [Github.WorkflowType], + Triggers = [WorkflowTriggers.Manual], + Targets = [new(nameof(IDuplicateDependencyTarget.DuplicateDependencyTarget1))], + Types = [WorkflowTypes.Devops.Pipeline], }, ]; + + public IReadOnlyList Options => [BuildOptions.Artifacts.UseCustomProvider]; } [ConfigureHostBuilder] @@ -25,27 +28,27 @@ public partial interface IDuplicateDependencyTarget : IStoreArtifact, IRetrieveA .ConsumesVariable(nameof(SetupBuildInfo), nameof(BuildId)) .ProducesArtifact("artifact-name"); - protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) => + protected static partial void ConfigureBuilderFromIDuplicateDependencyTarget(IHostApplicationBuilder builder) => builder.Services.AddSingleton(); } internal sealed class TestArtifactProvider : IArtifactProvider { public Task StoreArtifacts( - IReadOnlyList artifactNames, + IEnumerable artifactNames, string? buildId = null, string? buildSlice = null, CancellationToken cancellationToken = default) => throw new(); public Task RetrieveArtifacts( - IReadOnlyList artifactNames, + IEnumerable artifactNames, string? buildId = null, string? buildSlice = null, CancellationToken cancellationToken = default) => throw new(); - public Task Cleanup(IReadOnlyList runIdentifiers, CancellationToken cancellationToken = default) => + public Task Cleanup(IEnumerable runIdentifiers, CancellationToken cancellationToken = default) => throw new(); public Task> GetStoredRunIdentifiers( diff --git a/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/EnvironmentBuild.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/EnvironmentBuild.cs new file mode 100644 index 00000000..7c6eb3f8 --- /dev/null +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/EnvironmentBuild.cs @@ -0,0 +1,26 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class EnvironmentBuild : WorkflowBuildDefinition, IDevopsWorkflows, IEnvironmentTarget +{ + public override IReadOnlyList Workflows => + [ + new("environment-workflow") + { + Triggers = [WorkflowTriggers.Manual], + Targets = + [ + new(nameof(IEnvironmentTarget.EnvironmentTarget)) + { + Options = [BuildOptions.Deploy.ToEnvironment("test-env-1")], + }, + ], + Types = [WorkflowTypes.Devops.Pipeline], + }, + ]; +} + +public interface IEnvironmentTarget +{ + Target EnvironmentTarget => t => t; +} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/ManualInputBuild.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/ManualInputBuild.cs similarity index 58% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/ManualInputBuild.cs rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/ManualInputBuild.cs index dbc7d23b..1ece2b6a 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/ManualInputBuild.cs +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/ManualInputBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; +namespace Invex.Atom.Module.DevopsWorkflows.Tests.Workflows; [BuildDefinition] -public partial class ManualInputBuild : MinimalBuildDefinition, IGithubWorkflows, IManualInputTarget +public partial class ManualInputBuild : WorkflowBuildDefinition, IDevopsWorkflows, IManualInputTarget { public override IReadOnlyList Workflows => [ @@ -13,19 +13,24 @@ public partial class ManualInputBuild : MinimalBuildDefinition, IGithubWorkflows { Inputs = [ - ManualStringInput.ForParam(ParamDefinitions[Params.StringParamWithoutDefault]), - ManualStringInput.ForParam(ParamDefinitions[Params.StringParamWithDefault]), - ManualBoolInput.ForParam(ParamDefinitions[Params.BoolParamWithoutDefault]), - ManualBoolInput.ForParam(ParamDefinitions[Params.BoolParamWithDefault]), - ManualChoiceInput.ForParam(ParamDefinitions[Params.ChoiceParamWithoutDefault], + ManualStringInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.StringParamWithoutDefault)]), + ManualStringInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.StringParamWithDefault)]), + ManualBoolInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.BoolParamWithoutDefault)]), + ManualBoolInput.ForParam(ParamDefinitions[nameof(IManualInputTarget.BoolParamWithDefault)]), + ManualChoiceInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.ChoiceParamWithoutDefault)], ["choice 1", "choice 2", "choice 3"]), - ManualChoiceInput.ForParam(ParamDefinitions[Params.ChoiceParamWithDefault], + ManualChoiceInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.ChoiceParamWithDefault)], ["choice 1", "choice 2", "choice 3"]), ], }, ], - Targets = [WorkflowTargets.ManualInputTarget], - WorkflowTypes = [Github.WorkflowType], + Targets = [new(nameof(IManualInputTarget.ManualInputTarget))], + Types = [WorkflowTypes.Devops.Pipeline], }, ]; } diff --git a/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/MinimalBuild.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/MinimalBuild.cs new file mode 100644 index 00000000..419e0f27 --- /dev/null +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/MinimalBuild.cs @@ -0,0 +1,7 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class MinimalBuild : WorkflowBuildDefinition +{ + public override IReadOnlyList Workflows => []; +} diff --git a/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/SetupDotnetBuild.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/SetupDotnetBuild.cs new file mode 100644 index 00000000..3d2064f5 --- /dev/null +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/SetupDotnetBuild.cs @@ -0,0 +1,26 @@ +namespace Invex.Atom.Module.DevopsWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class SetupDotnetBuild : WorkflowBuildDefinition, IDevopsWorkflows, ISetupDotnetTarget +{ + public override IReadOnlyList Workflows => + [ + new("setup-dotnet") + { + Triggers = [WorkflowTriggers.PushToMain], + Targets = + [ + new(nameof(ISetupDotnetTarget.SetupDotnetTarget)) + { + Options = [BuildOptions.Steps.SetupDotnet.Dotnet90X()], + }, + ], + Types = [WorkflowTypes.Devops.Pipeline], + }, + ]; +} + +public interface ISetupDotnetTarget +{ + Target SetupDotnetTarget => t => t; +} diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/SimpleBuild.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/SimpleBuild.cs similarity index 59% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/SimpleBuild.cs rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/SimpleBuild.cs index c8341aa0..ddd3cbc7 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/SimpleBuild.cs +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/SimpleBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; +namespace Invex.Atom.Module.DevopsWorkflows.Tests.Workflows; [BuildDefinition] -public partial class SimpleBuild : MinimalBuildDefinition, IGithubWorkflows, ISimpleTarget +public partial class SimpleBuild : WorkflowBuildDefinition, IDevopsWorkflows, ISimpleTarget { public override IReadOnlyList Workflows => [ @@ -14,8 +14,8 @@ public partial class SimpleBuild : MinimalBuildDefinition, IGithubWorkflows, ISi IncludedBranches = ["main"], }, ], - Targets = [WorkflowTargets.SimpleTarget], - WorkflowTypes = [new GithubWorkflowType()], + Targets = [new(nameof(ISimpleTarget.SimpleTarget))], + Types = [WorkflowTypes.Devops.Pipeline], }, ]; } diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ArtifactBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ArtifactBuild_GeneratesWorkflow.verified.txt similarity index 66% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ArtifactBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ArtifactBuild_GeneratesWorkflow.verified.txt index c6044e7e..b5fd6b60 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ArtifactBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ArtifactBuild_GeneratesWorkflow.verified.txt @@ -1,111 +1,124 @@ name: artifact-workflow trigger: none +pr: + branches: + include: [ main ] jobs: - + - job: ArtifactTarget1 + displayName: ArtifactTarget1 pool: vmImage: ubuntu-latest steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - script: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget1 --skip --headless + displayName: ArtifactTarget1 name: ArtifactTarget1 - + - task: PublishPipelineArtifact@1 displayName: TestArtifact1 inputs: artifactName: TestArtifact1 - targetPath: "$(Build.BinariesDirectory)/TestArtifact1" - - + targetPath: $(Build.BinariesDirectory)/TestArtifact1 + - job: ArtifactTarget2 + displayName: ArtifactTarget2 dependsOn: [ ArtifactTarget1 ] pool: vmImage: ubuntu-latest steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - task: DownloadPipelineArtifact@2 displayName: TestArtifact1 inputs: artifact: TestArtifact1 - path: "$(Build.ArtifactStagingDirectory)/TestArtifact1" - + path: $(Build.ArtifactStagingDirectory)/TestArtifact1 + - script: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget2 --skip --headless + displayName: ArtifactTarget2 name: ArtifactTarget2 - + - task: PublishPipelineArtifact@1 displayName: TestArtifact2 inputs: artifactName: TestArtifact2-Slice1 - targetPath: "$(Build.BinariesDirectory)/TestArtifact2" - + targetPath: $(Build.BinariesDirectory)/TestArtifact2 + - task: PublishPipelineArtifact@1 displayName: TestArtifact2 inputs: artifactName: TestArtifact2-Slice2 - targetPath: "$(Build.BinariesDirectory)/TestArtifact2" - - + targetPath: $(Build.BinariesDirectory)/TestArtifact2 + - job: ArtifactTarget3 + displayName: ArtifactTarget3 dependsOn: [ ArtifactTarget1, ArtifactTarget2 ] pool: vmImage: ubuntu-latest steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - task: DownloadPipelineArtifact@2 displayName: TestArtifact1 inputs: artifact: TestArtifact1 - path: "$(Build.ArtifactStagingDirectory)/TestArtifact1" - + path: $(Build.ArtifactStagingDirectory)/TestArtifact1 + - task: DownloadPipelineArtifact@2 displayName: TestArtifact2 inputs: artifact: TestArtifact2-Slice1 - path: "$(Build.ArtifactStagingDirectory)/TestArtifact2" - + path: $(Build.ArtifactStagingDirectory)/TestArtifact2 + - task: DownloadPipelineArtifact@2 displayName: TestArtifact2 inputs: artifact: TestArtifact2-Slice2 - path: "$(Build.ArtifactStagingDirectory)/TestArtifact2" - + path: $(Build.ArtifactStagingDirectory)/TestArtifact2 + - script: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget3 --skip --headless + displayName: ArtifactTarget3 name: ArtifactTarget3 - + - job: ArtifactTarget4 + displayName: ArtifactTarget4 dependsOn: [ ArtifactTarget2 ] strategy: matrix: 001_Slice1: - slice: 'Slice1' + test-slice: 'Slice1' 002_Slice2: - slice: 'Slice2' + test-slice: 'Slice2' pool: vmImage: ubuntu-latest steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - task: DownloadPipelineArtifact@2 displayName: TestArtifact2 inputs: - artifact: TestArtifact2-$(slice) - path: "$(Build.ArtifactStagingDirectory)/TestArtifact2" - + artifact: TestArtifact2-$(test-slice) + path: $(Build.ArtifactStagingDirectory)/TestArtifact2 + - script: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget4 --skip --headless + displayName: ArtifactTarget4 name: ArtifactTarget4 env: - slice: $(slice) - build-slice: $(slice) + test-slice: $(test-slice) + build-slice: $(test-slice) diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.CheckoutOptionsBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.CheckoutOptionsBuild_GeneratesWorkflow.verified.txt similarity index 66% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.CheckoutOptionsBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.CheckoutOptionsBuild_GeneratesWorkflow.verified.txt index 9c3edf4f..efae9561 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.CheckoutOptionsBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.CheckoutOptionsBuild_GeneratesWorkflow.verified.txt @@ -1,17 +1,18 @@ name: checkoutoption-workflow +trigger: none jobs: - + - job: CheckoutOptionTarget + displayName: CheckoutOptionTarget pool: vmImage: ubuntu-latest steps: - + - checkout: self - fetchDepth: 0 - lfs: true - submodules: recursive - + displayName: Checkout with no options + - script: dotnet run --project AtomTest/AtomTest.csproj CheckoutOptionTarget --skip --headless + displayName: CheckoutOptionTarget name: CheckoutOptionTarget diff --git a/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.CustomArtifactBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.CustomArtifactBuild_GeneratesWorkflow.verified.txt new file mode 100644 index 00000000..bc3967df --- /dev/null +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.CustomArtifactBuild_GeneratesWorkflow.verified.txt @@ -0,0 +1,173 @@ +name: custom-artifact-workflow + +trigger: none +pr: + branches: + include: [ main ] + +jobs: + + - job: SetupBuildInfo + displayName: SetupBuildInfo + pool: + vmImage: ubuntu-latest + steps: + + - checkout: self + enabled: true + fetchDepth: 0 + + - script: dotnet run --project AtomTest/AtomTest.csproj SetupBuildInfo --skip --headless + displayName: SetupBuildInfo + name: SetupBuildInfo + + - job: ArtifactTarget1 + displayName: ArtifactTarget1 + dependsOn: [ SetupBuildInfo ] + variables: + - name: build-name + value: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-name'] ] + - name: build-id + value: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-id'] ] + pool: + vmImage: ubuntu-latest + steps: + + - checkout: self + enabled: true + fetchDepth: 0 + + - script: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget1 --skip --headless + displayName: ArtifactTarget1 + name: ArtifactTarget1 + + - script: dotnet run --project AtomTest/AtomTest.csproj StoreArtifact --skip --headless + displayName: Store artifact `TestArtifact1` + env: + build-name: $(build-name) + build-id: $(build-id) + atom-artifacts: TestArtifact1 + + - job: ArtifactTarget2 + displayName: ArtifactTarget2 + dependsOn: [ ArtifactTarget1, SetupBuildInfo ] + variables: + - name: build-name + value: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-name'] ] + - name: build-id + value: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-id'] ] + pool: + vmImage: ubuntu-latest + steps: + + - checkout: self + enabled: true + fetchDepth: 0 + + - script: dotnet run --project AtomTest/AtomTest.csproj RetrieveArtifact --skip --headless + displayName: Retrieve artifact `TestArtifact1` + env: + build-name: $(build-name) + build-id: $(build-id) + atom-artifacts: TestArtifact1 + + - script: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget2 --skip --headless + displayName: ArtifactTarget2 + name: ArtifactTarget2 + + - script: dotnet run --project AtomTest/AtomTest.csproj StoreArtifact --skip --headless + displayName: Store artifact `TestArtifact2` + env: + build-name: $(build-name) + build-id: $(build-id) + atom-artifacts: TestArtifact2 + build-slice: Slice1 + + - script: dotnet run --project AtomTest/AtomTest.csproj StoreArtifact --skip --headless + displayName: Store artifact `TestArtifact2` + env: + build-name: $(build-name) + build-id: $(build-id) + atom-artifacts: TestArtifact2 + build-slice: Slice2 + + - job: ArtifactTarget3 + displayName: ArtifactTarget3 + dependsOn: [ ArtifactTarget1, ArtifactTarget2, SetupBuildInfo ] + variables: + - name: build-name + value: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-name'] ] + - name: build-id + value: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-id'] ] + pool: + vmImage: ubuntu-latest + steps: + + - checkout: self + enabled: true + fetchDepth: 0 + + - script: dotnet run --project AtomTest/AtomTest.csproj RetrieveArtifact --skip --headless + displayName: Retrieve artifact `TestArtifact1` + env: + build-name: $(build-name) + build-id: $(build-id) + atom-artifacts: TestArtifact1 + + - script: dotnet run --project AtomTest/AtomTest.csproj RetrieveArtifact --skip --headless + displayName: Retrieve artifact `TestArtifact2` + env: + build-name: $(build-name) + build-id: $(build-id) + atom-artifacts: TestArtifact2 + build-slice: Slice1 + + - script: dotnet run --project AtomTest/AtomTest.csproj RetrieveArtifact --skip --headless + displayName: Retrieve artifact `TestArtifact2` + env: + build-name: $(build-name) + build-id: $(build-id) + atom-artifacts: TestArtifact2 + build-slice: Slice2 + + - script: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget3 --skip --headless + displayName: ArtifactTarget3 + name: ArtifactTarget3 + + - job: ArtifactTarget4 + displayName: ArtifactTarget4 + dependsOn: [ ArtifactTarget2, SetupBuildInfo ] + variables: + - name: build-name + value: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-name'] ] + - name: build-id + value: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-id'] ] + strategy: + matrix: + 001_Slice1: + test-slice: 'Slice1' + 002_Slice2: + test-slice: 'Slice2' + pool: + vmImage: ubuntu-latest + steps: + + - checkout: self + enabled: true + fetchDepth: 0 + + - script: dotnet run --project AtomTest/AtomTest.csproj RetrieveArtifact --skip --headless + displayName: Retrieve artifact `TestArtifact2` + env: + build-name: $(build-name) + build-id: $(build-id) + test-slice: $(test-slice) + build-slice: $(test-slice) + atom-artifacts: TestArtifact2 + + - script: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget4 --skip --headless + displayName: ArtifactTarget4 + name: ArtifactTarget4 + env: + test-slice: $(test-slice) + build-slice: $(test-slice) diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.DependentBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.DependentBuild_GeneratesWorkflow.verified.txt similarity index 71% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.DependentBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.DependentBuild_GeneratesWorkflow.verified.txt index fbac3f40..b26c7f2f 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.DependentBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.DependentBuild_GeneratesWorkflow.verified.txt @@ -1,28 +1,37 @@ name: dependent-workflow trigger: none +pr: + branches: + include: [ main ] jobs: - + - job: DependentTarget1 + displayName: DependentTarget1 pool: vmImage: ubuntu-latest steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - script: dotnet run --project AtomTest/AtomTest.csproj DependentTarget1 --skip --headless + displayName: DependentTarget1 name: DependentTarget1 - + - job: DependentTarget2 + displayName: DependentTarget2 dependsOn: [ DependentTarget1 ] pool: vmImage: ubuntu-latest steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - script: dotnet run --project AtomTest/AtomTest.csproj DependentTarget2 --skip --headless + displayName: DependentTarget2 name: DependentTarget2 diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.DuplicateDependencyBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.DuplicateDependencyBuild_GeneratesWorkflow.verified.txt similarity index 65% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.DuplicateDependencyBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.DuplicateDependencyBuild_GeneratesWorkflow.verified.txt index fcae51e3..cf09a8d6 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.DuplicateDependencyBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.DuplicateDependencyBuild_GeneratesWorkflow.verified.txt @@ -1,38 +1,48 @@ name: duplicatedependency-workflow +trigger: none jobs: - + - job: SetupBuildInfo + displayName: SetupBuildInfo pool: vmImage: ubuntu-latest steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - script: dotnet run --project AtomTest/AtomTest.csproj SetupBuildInfo --skip --headless + displayName: SetupBuildInfo name: SetupBuildInfo - + - job: DuplicateDependencyTarget1 + displayName: DuplicateDependencyTarget1 dependsOn: [ SetupBuildInfo, SetupBuildInfo ] + variables: + - name: build-name + value: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-name'] ] + - name: build-id + value: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-id'] ] pool: vmImage: ubuntu-latest - variables: - build-name: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-name'] ] - build-id: $[ dependencies.SetupBuildInfo.outputs['SetupBuildInfo.build-id'] ] steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - script: dotnet run --project AtomTest/AtomTest.csproj DuplicateDependencyTarget1 --skip --headless + displayName: DuplicateDependencyTarget1 name: DuplicateDependencyTarget1 env: build-name: $(build-name) build-id: $(build-id) - + - script: dotnet run --project AtomTest/AtomTest.csproj StoreArtifact --skip --headless + displayName: Store artifact `artifact-name` env: build-name: $(build-name) build-id: $(build-id) diff --git a/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.EnvironmentBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.EnvironmentBuild_GeneratesWorkflow.verified.txt new file mode 100644 index 00000000..1cadbeec --- /dev/null +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.EnvironmentBuild_GeneratesWorkflow.verified.txt @@ -0,0 +1,23 @@ +name: environment-workflow + +trigger: none + +jobs: + + - deployment: EnvironmentTarget + displayName: EnvironmentTarget + pool: + vmImage: ubuntu-latest + environment: test-env-1 + strategy: + runOnce: + deploy: + steps: + + - checkout: self + enabled: true + fetchDepth: 0 + + - script: dotnet run --project AtomTest/AtomTest.csproj EnvironmentTarget --skip --headless + displayName: EnvironmentTarget + name: EnvironmentTarget diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ManualInputBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ManualInputBuild_GeneratesWorkflow.verified.txt similarity index 50% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ManualInputBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ManualInputBuild_GeneratesWorkflow.verified.txt index 867500af..2f2432b4 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ManualInputBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.ManualInputBuild_GeneratesWorkflow.verified.txt @@ -1,44 +1,43 @@ name: manual-input-workflow +trigger: none parameters: - name: string-param-without-default - displayName: 'string-param-without-default | String param' + displayName: string-param-without-default | String param type: string - name: string-param-with-default - displayName: 'string-param-with-default | String param' + displayName: string-param-with-default | String param type: string + default: default-value - name: bool-param-without-default - displayName: 'bool-param-without-default | Bool param' + displayName: bool-param-without-default | Bool param type: boolean - name: bool-param-with-default - displayName: 'bool-param-with-default | Bool param' + displayName: bool-param-with-default | Bool param type: boolean + default: true - name: choice-param-without-default - displayName: 'choice-param-without-default | Choice param' + displayName: choice-param-without-default | Choice param type: string - default: 'choice 1' - values: - - 'choice 1' - - 'choice 2' - - 'choice 3' + values: [ choice 1, choice 2, choice 3 ] - name: choice-param-with-default - displayName: 'choice-param-with-default | Choice param' + displayName: choice-param-with-default | Choice param type: string - default: 'choice 1' - values: - - 'choice 1' - - 'choice 2' - - 'choice 3' + default: choice 1 + values: [ choice 1, choice 2, choice 3 ] jobs: - + - job: ManualInputTarget + displayName: ManualInputTarget pool: vmImage: ubuntu-latest steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - script: dotnet run --project AtomTest/AtomTest.csproj ManualInputTarget --skip --headless + displayName: ManualInputTarget name: ManualInputTarget diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.SetupDotnetBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.SetupDotnetBuild_GeneratesWorkflow.verified.txt similarity index 65% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.SetupDotnetBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.SetupDotnetBuild_GeneratesWorkflow.verified.txt index 839a1a95..06fd73e1 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.SetupDotnetBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.SetupDotnetBuild_GeneratesWorkflow.verified.txt @@ -2,22 +2,25 @@ trigger: branches: - include: - - 'main' + include: [ main ] jobs: - + - job: SetupDotnetTarget + displayName: SetupDotnetTarget pool: vmImage: ubuntu-latest steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - task: UseDotNet@2 + displayName: Setup .NET 9.0.x inputs: - version: '9.0.x' + version: 9.0.x - script: dotnet run --project AtomTest/AtomTest.csproj SetupDotnetTarget --skip --headless + displayName: SetupDotnetTarget name: SetupDotnetTarget diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.SimpleBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.SimpleBuild_GeneratesWorkflow.verified.txt similarity index 69% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.SimpleBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.SimpleBuild_GeneratesWorkflow.verified.txt index 61c431e3..06213ed7 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.SimpleBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.SimpleBuild_GeneratesWorkflow.verified.txt @@ -1,16 +1,22 @@ name: simple-workflow trigger: none +pr: + branches: + include: [ main ] jobs: - + - job: SimpleTarget + displayName: SimpleTarget pool: vmImage: ubuntu-latest steps: - + - checkout: self + enabled: true fetchDepth: 0 - + - script: dotnet run --project AtomTest/AtomTest.csproj SimpleTarget --skip --headless + displayName: SimpleTarget name: SimpleTarget diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.cs similarity index 88% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.cs rename to tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.cs index 10dd639d..4a4d14af 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.cs +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/Workflows/WorkflowTests.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Tests.Workflows; +namespace Invex.Atom.Module.DevopsWorkflows.Tests.Workflows; [TestFixture] -public class WorkflowTests +internal sealed class WorkflowTests { private static string WorkflowDir => Environment.OSVersion.Platform is PlatformID.Win32NT @@ -13,7 +13,9 @@ public void MinimalBuild_GeneratesNoWorkflows() { // Arrange var fileSystem = FileSystemUtils.DefaultMockFileSystem; - var build = CreateTestHost(fileSystem: fileSystem, commandLineArgs: new(true, [new GenArg()])); + + var build = CreateTestHost(fileSystem: fileSystem, + commandLineArgs: new(true, [new CommandArg(nameof(IGen.Gen))])); // Act build.Run(); @@ -31,7 +33,9 @@ public async Task SimpleBuild_GeneratesWorkflow() { // Arrange var fileSystem = FileSystemUtils.DefaultMockFileSystem; - var build = CreateTestHost(fileSystem: fileSystem, commandLineArgs: new(true, [new GenArg()])); + + var build = CreateTestHost(fileSystem: fileSystem, + commandLineArgs: new(true, [new CommandArg(nameof(IGen.Gen))])); // Act await build.RunAsync(); @@ -54,7 +58,9 @@ public async Task DependentBuild_GeneratesWorkflow() { // Arrange var fileSystem = FileSystemUtils.DefaultMockFileSystem; - var build = CreateTestHost(fileSystem: fileSystem, commandLineArgs: new(true, [new GenArg()])); + + var build = CreateTestHost(fileSystem: fileSystem, + commandLineArgs: new(true, [new CommandArg(nameof(IGen.Gen))])); // Act await build.RunAsync(); @@ -78,7 +84,8 @@ public async Task ArtifactBuild_GeneratesWorkflow() // Arrange var fileSystem = FileSystemUtils.DefaultMockFileSystem; var console = new TestConsole(); - var build = CreateTestHost(console, fileSystem, new(true, [new GenArg()])); + + var build = CreateTestHost(console, fileSystem, new(true, [new CommandArg(nameof(IGen.Gen))])); // Act await build.RunAsync(); @@ -105,7 +112,7 @@ public async Task CustomArtifactBuild_GeneratesWorkflow() var build = CreateTestHost(console, fileSystem, - new(true, [new GenArg()]), + new(true, [new CommandArg(nameof(IGen.Gen))]), configure: builder => builder.Services.AddSingleton()); // Act @@ -131,7 +138,7 @@ public async Task ManualInputBuild_GeneratesWorkflow() var fileSystem = FileSystemUtils.DefaultMockFileSystem; var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); + commandLineArgs: new(true, [new CommandArg(nameof(IGen.Gen))])); // Act await build.RunAsync(); @@ -156,7 +163,7 @@ public async Task SetupDotnetBuild_GeneratesWorkflow() var fileSystem = FileSystemUtils.DefaultMockFileSystem; var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); + commandLineArgs: new(true, [new CommandArg(nameof(IGen.Gen))])); // Act await build.RunAsync(); @@ -181,7 +188,7 @@ public async Task EnvironmentBuild_GeneratesWorkflow() var fileSystem = FileSystemUtils.DefaultMockFileSystem; var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); + commandLineArgs: new(true, [new CommandArg(nameof(IGen.Gen))])); // Act await build.RunAsync(); @@ -206,7 +213,7 @@ public async Task CheckoutOptionsBuild_GeneratesWorkflow() var fileSystem = FileSystemUtils.DefaultMockFileSystem; var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); + commandLineArgs: new(true, [new CommandArg(nameof(IGen.Gen))])); // Act await build.RunAsync(); @@ -231,7 +238,7 @@ public async Task DuplicateDependencyBuild_GeneratesWorkflow() var fileSystem = FileSystemUtils.DefaultMockFileSystem; var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); + commandLineArgs: new(true, [new CommandArg(nameof(IGen.Gen))])); // Act await build.RunAsync(); diff --git a/tests/Invex.Atom.Module.DevopsWorkflows.Tests/_usings.cs b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/_usings.cs new file mode 100644 index 00000000..835cc1da --- /dev/null +++ b/tests/Invex.Atom.Module.DevopsWorkflows.Tests/_usings.cs @@ -0,0 +1,20 @@ +global using Invex.Atom.Build; +global using Invex.Atom.Build.Args; +global using Invex.Atom.Build.Artifacts; +global using Invex.Atom.Build.BuildOptions; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Build.Params; +global using Invex.Atom.Module.DevopsWorkflows.Extensions; +global using Invex.Atom.TestUtils; +global using Invex.Atom.Workflows; +global using Invex.Atom.Workflows.Definition; +global using Invex.Atom.Workflows.Definition.Triggers; +global using Invex.Atom.Workflows.Options; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using NUnit.Framework; +global using Shouldly; +global using Spectre.Console.Testing; +global using static Invex.Atom.TestUtils.TestUtils; +global using Target = Invex.Atom.Build.Definition.Target; diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/DecSm.Atom.Module.GithubWorkflows.Tests.csproj b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Invex.Atom.Module.GithubWorkflows.Tests.csproj similarity index 85% rename from DecSm.Atom.Module.GithubWorkflows.Tests/DecSm.Atom.Module.GithubWorkflows.Tests.csproj rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Invex.Atom.Module.GithubWorkflows.Tests.csproj index dc239153..cf17f901 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/DecSm.Atom.Module.GithubWorkflows.Tests.csproj +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Invex.Atom.Module.GithubWorkflows.Tests.csproj @@ -3,7 +3,7 @@ net10.0;net9.0;net8.0 false - decsm.atom.tests + invex-atom-module-githubworkflows-tests @@ -17,7 +17,7 @@ - + @@ -50,9 +50,9 @@ - - - + + + diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/ArtifactBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ArtifactBuild.cs similarity index 59% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/ArtifactBuild.cs rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ArtifactBuild.cs index c990feb1..844f6a48 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/ArtifactBuild.cs +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ArtifactBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; [BuildDefinition] -public partial class ArtifactBuild : MinimalBuildDefinition, +public partial class ArtifactBuild : WorkflowBuildDefinition, IGithubWorkflows, IArtifactTarget1, IArtifactTarget2, @@ -21,22 +21,27 @@ public partial class ArtifactBuild : MinimalBuildDefinition, ], Targets = [ - WorkflowTargets.ArtifactTarget1, - WorkflowTargets.ArtifactTarget2, - WorkflowTargets.ArtifactTarget3, - WorkflowTargets.ArtifactTarget4.WithMatrixDimensions( - new MatrixDimension(nameof(IArtifactSliceTarget1.Slice)) - { - Values = [IArtifactTarget2.Slice1, IArtifactTarget2.Slice2], - }), + new(nameof(IArtifactTarget1.ArtifactTarget1)), + new(nameof(IArtifactTarget2.ArtifactTarget2)), + new(nameof(IArtifactTarget3.ArtifactTarget3)), + new(nameof(IArtifactTarget4.ArtifactTarget4)) + { + MatrixDimensions = + [ + new(nameof(IArtifactSliceTarget1.TestSlice)) + { + Values = [IArtifactTarget2.Slice1, IArtifactTarget2.Slice2], + }, + ], + }, ], - WorkflowTypes = [new GithubWorkflowType()], + Types = [WorkflowTypes.Github.Action], }, ]; } [BuildDefinition] -public partial class CustomArtifactBuild : MinimalBuildDefinition, +public partial class CustomArtifactBuild : WorkflowBuildDefinition, IGithubWorkflows, IStoreArtifact, IRetrieveArtifact, @@ -58,18 +63,23 @@ public partial class CustomArtifactBuild : MinimalBuildDefinition, ], Targets = [ - WorkflowTargets.SetupBuildInfo, - WorkflowTargets.ArtifactTarget1, - WorkflowTargets.ArtifactTarget2, - WorkflowTargets.ArtifactTarget3, - WorkflowTargets.ArtifactTarget4.WithMatrixDimensions( - new MatrixDimension(nameof(IArtifactSliceTarget1.Slice)) - { - Values = [IArtifactTarget2.Slice1, IArtifactTarget2.Slice2], - }), + new(nameof(ISetupBuildInfo.SetupBuildInfo)), + new(nameof(IArtifactTarget1.ArtifactTarget1)), + new(nameof(IArtifactTarget2.ArtifactTarget2)), + new(nameof(IArtifactTarget3.ArtifactTarget3)), + new(nameof(IArtifactTarget4.ArtifactTarget4)) + { + MatrixDimensions = + [ + new(nameof(IArtifactSliceTarget1.TestSlice)) + { + Values = [IArtifactTarget2.Slice1, IArtifactTarget2.Slice2], + }, + ], + }, ], - WorkflowTypes = [new GithubWorkflowType()], - Options = [UseCustomArtifactProvider.Enabled], + Types = [WorkflowTypes.Github.Action], + Options = [BuildOptions.Artifacts.UseCustomProvider], }, ]; } @@ -79,8 +89,8 @@ public interface IArtifactSliceTarget1 : IBuildAccessor const string Slice1 = "Slice1"; const string Slice2 = "Slice2"; - [ParamDefinition("slice", "Slice")] - string Slice => GetParam(() => Slice)!; + [ParamDefinition("test-slice", "Test slice")] + string TestSlice => GetParam(() => TestSlice)!; } public interface IArtifactTarget1 diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/CheckoutOptionBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/CheckoutOptionBuild.cs new file mode 100644 index 00000000..6cca786b --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/CheckoutOptionBuild.cs @@ -0,0 +1,50 @@ +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class CheckoutOptionBuild : WorkflowBuildDefinition, IGithubWorkflows, ICheckoutOptionTarget +{ + public override IReadOnlyList Workflows => + [ + new("checkoutoption-workflow") + { + Triggers = [WorkflowTriggers.Manual], + Targets = + [ + new(nameof(ICheckoutOptionTarget.CheckoutOptionTarget1)), + new(nameof(ICheckoutOptionTarget.CheckoutOptionTarget2)) + { + Options = + [ + BuildOptions.Github.Steps.Checkout(new() + { + Submodules = "recursive", + Token = "some-token", + FetchDepth = 0, + Lfs = true, + }), + ], + }, + new(nameof(ICheckoutOptionTarget.CheckoutOptionTarget3)) + { + Options = + [ + BuildOptions.Github.Steps.Checkout(new() + { + Enabled = false, + }), + ], + }, + ], + Types = [WorkflowTypes.Github.Action], + }, + ]; +} + +public interface ICheckoutOptionTarget +{ + Target CheckoutOptionTarget1 => t => t; + + Target CheckoutOptionTarget2 => t => t; + + Target CheckoutOptionTarget3 => t => t; +} diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/DependabotBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/DependabotBuild.cs new file mode 100644 index 00000000..ed61d1c0 --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/DependabotBuild.cs @@ -0,0 +1,98 @@ +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class DependabotBuild : WorkflowBuildDefinition, IGithubWorkflows +{ + public override IReadOnlyList Workflows => + [ + WorkflowPresets.Github.Dependabot(new() + { + Registries = new Dictionary + { + ["registry-1"] = new() + { + Type = RegistryType.CargoRegistry, + Url = "url1", + }, + }, + Updates = + [ + new() + { + Allow = + [ + new() + { + DependencyName = "dep-name", + DependencyType = DependencyType.All, + }, + ], + Assignees = ["assignee1"], + CommitMessage = new() + { + Include = CommitMessageInclude.Scope, + Prefix = "prefix", + PrefixDevelopment = "prefix-dev", + }, + Cooldown = new() + { + DefaultDays = 1, + SemverMajorDays = 2, + SemverMinorDays = 3, + SemverPatchDays = 4, + Include = ["include1"], + Exclude = ["exclude1"], + }, + Directories = ["directory1", "directory2"], + Directory = "directory", + ExcludePaths = ["exclude-path1", "exclude-path2"], + Groups = new Dictionary + { + ["group-1"] = new DependabotGroup.FromPatterns + { + AppliesTo = GroupAppliesTo.SecurityUpdates, + DependencyType = GroupDependencyType.Production, + ExcludePatterns = ["exclude-pattern-1", "exclude-pattern-2"], + UpdateTypes = [GroupUpdateType.Major, GroupUpdateType.Minor], + GroupBy = GroupBy.DependencyName, + Patterns = ["pattern-1", "pattern-2"], + }, + }, + Ignore = + [ + new() + { + DependencyName = "dep-name", + UpdateTypes = [SemverUpdateType.VersionUpdateSemverMajor], + Versions = new DependabotVersions.Multiple(["1.0.0", "2.0.0"]), + }, + ], + InsecureExternalCodeExecution = InsecureExternalCodeExecution.Allow, + Labels = ["label1", "label2"], + Milestone = 1, + Name = "update-deps", + OpenPullRequestsLimit = 5, + PackageEcosystem = "package-ecosystem-1", + PullRequestBranchName = new() + { + Separator = BranchNameSeparator.Hyphen, + }, + RebaseStrategy = RebaseStrategy.Auto, + Registries = new DependabotRegistries.All(), + Schedule = new() + { + Interval = ScheduleInterval.Daily, + Day = ScheduleDay.Monday, + Time = "02:00", + Timezone = "UTC", + }, + TargetBranch = "main", + Vendor = true, + VersioningStrategy = VersioningStrategy.Increase, + Patterns = ["pattern1", "pattern2"], + MultiEcosystemGroup = "multi-ecosystem-group", + }, + ], + }), + ]; +} diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/DependentBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/DependentBuild.cs similarity index 58% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/DependentBuild.cs rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/DependentBuild.cs index c70bcc4c..7054cd37 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/DependentBuild.cs +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/DependentBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Tests.Workflows; +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; [BuildDefinition] -public partial class DependentBuild : MinimalBuildDefinition, IDevopsWorkflows, IDependentTarget1, IDependentTarget2 +public partial class DependentBuild : WorkflowBuildDefinition, IGithubWorkflows, IDependentTarget1, IDependentTarget2 { public override IReadOnlyList Workflows => [ @@ -14,8 +14,11 @@ public partial class DependentBuild : MinimalBuildDefinition, IDevopsWorkflows, IncludedBranches = ["main"], }, ], - Targets = [WorkflowTargets.DependentTarget1, WorkflowTargets.DependentTarget2], - WorkflowTypes = [Devops.WorkflowType], + Targets = + [ + new(nameof(IDependentTarget1.DependentTarget1)), new(nameof(IDependentTarget2.DependentTarget2)), + ], + Types = [WorkflowTypes.Github.Action], }, ]; } diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/DuplicateDependencyBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/DuplicateDependencyBuild.cs similarity index 62% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/DuplicateDependencyBuild.cs rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/DuplicateDependencyBuild.cs index 4745f604..2334ca92 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/DuplicateDependencyBuild.cs +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/DuplicateDependencyBuild.cs @@ -1,19 +1,19 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Tests.Workflows; +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; [BuildDefinition] -public partial class DuplicateDependencyBuild : MinimalBuildDefinition, IDevopsWorkflows, IDuplicateDependencyTarget +public partial class DuplicateDependencyBuild : WorkflowBuildDefinition, IGithubWorkflows, IDuplicateDependencyTarget { - public override IReadOnlyList GlobalWorkflowOptions => [UseCustomArtifactProvider.Enabled]; - public override IReadOnlyList Workflows => [ new("duplicatedependency-workflow") { - Triggers = [ManualTrigger.Empty], - Targets = [WorkflowTargets.DuplicateDependencyTarget1], - WorkflowTypes = [Devops.WorkflowType], + Triggers = [WorkflowTriggers.Manual], + Targets = [new(nameof(IDuplicateDependencyTarget.DuplicateDependencyTarget1))], + Types = [WorkflowTypes.Github.Action], }, ]; + + public IReadOnlyList Options => [BuildOptions.Artifacts.UseCustomProvider]; } [ConfigureHostBuilder] @@ -25,27 +25,27 @@ public partial interface IDuplicateDependencyTarget : IStoreArtifact, IRetrieveA .ConsumesVariable(nameof(SetupBuildInfo), nameof(BuildId)) .ProducesArtifact("artifact-name"); - protected static partial void ConfigureBuilder(IHostApplicationBuilder builder) => + protected static partial void ConfigureBuilderFromIDuplicateDependencyTarget(IHostApplicationBuilder builder) => builder.Services.AddSingleton(); } internal sealed class TestArtifactProvider : IArtifactProvider { public Task StoreArtifacts( - IReadOnlyList artifactNames, + IEnumerable artifactNames, string? buildId = null, string? buildSlice = null, CancellationToken cancellationToken = default) => throw new(); public Task RetrieveArtifacts( - IReadOnlyList artifactNames, + IEnumerable artifactNames, string? buildId = null, string? buildSlice = null, CancellationToken cancellationToken = default) => throw new(); - public Task Cleanup(IReadOnlyList runIdentifiers, CancellationToken cancellationToken = default) => + public Task Cleanup(IEnumerable runIdentifiers, CancellationToken cancellationToken = default) => throw new(); public Task> GetStoredRunIdentifiers( diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/EmptyBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/EmptyBuild.cs new file mode 100644 index 00000000..1a641086 --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/EmptyBuild.cs @@ -0,0 +1,7 @@ +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class EmptyBuild : WorkflowBuildDefinition +{ + public override IReadOnlyList Workflows => []; +} diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/EnvironmentBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/EnvironmentBuild.cs new file mode 100644 index 00000000..dfdb9983 --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/EnvironmentBuild.cs @@ -0,0 +1,26 @@ +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class EnvironmentBuild : WorkflowBuildDefinition, IGithubWorkflows, IEnvironmentTarget +{ + public override IReadOnlyList Workflows => + [ + new("environment-workflow") + { + Triggers = [WorkflowTriggers.Manual], + Targets = + [ + new(nameof(IEnvironmentTarget.EnvironmentTarget)) + { + Options = [BuildOptions.Deploy.ToEnvironment("test-env-1")], + }, + ], + Types = [WorkflowTypes.Github.Action], + }, + ]; +} + +public interface IEnvironmentTarget +{ + Target EnvironmentTarget => t => t; +} diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.ArtifactBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.ArtifactBuild_GeneratesWorkflow.verified.txt new file mode 100644 index 00000000..2f444d1d --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.ArtifactBuild_GeneratesWorkflow.verified.txt @@ -0,0 +1,119 @@ +name: artifact-workflow + +on: + pull_request: + branches: [ main ] + +permissions: { } + +jobs: + + ArtifactTarget1: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: ArtifactTarget1 + id: ArtifactTarget1 + run: dotnet run --project AtomTest/AtomTest.csproj -- ArtifactTarget1 --skip --headless + + - name: Store TestArtifact1 + uses: actions/upload-artifact@v7 + with: + name: TestArtifact1 + path: '${{ github.workspace }}/.github/publish/TestArtifact1' + + ArtifactTarget2: + needs: [ ArtifactTarget1 ] + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Retrieve TestArtifact1 + uses: actions/download-artifact@v8 + with: + name: TestArtifact1 + path: '${{ github.workspace }}/.github/artifacts/TestArtifact1' + + - name: ArtifactTarget2 + id: ArtifactTarget2 + run: dotnet run --project AtomTest/AtomTest.csproj -- ArtifactTarget2 --skip --headless + + - name: Store TestArtifact2 + uses: actions/upload-artifact@v7 + with: + name: TestArtifact2-Slice1 + path: '${{ github.workspace }}/.github/publish/TestArtifact2' + + - name: Store TestArtifact2 + uses: actions/upload-artifact@v7 + with: + name: TestArtifact2-Slice2 + path: '${{ github.workspace }}/.github/publish/TestArtifact2' + + ArtifactTarget3: + needs: [ ArtifactTarget1, ArtifactTarget2 ] + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Retrieve TestArtifact1 + uses: actions/download-artifact@v8 + with: + name: TestArtifact1 + path: '${{ github.workspace }}/.github/artifacts/TestArtifact1' + + - name: Retrieve TestArtifact2 + uses: actions/download-artifact@v8 + with: + name: TestArtifact2-Slice1 + path: '${{ github.workspace }}/.github/artifacts/TestArtifact2' + + - name: Retrieve TestArtifact2 + uses: actions/download-artifact@v8 + with: + name: TestArtifact2-Slice2 + path: '${{ github.workspace }}/.github/artifacts/TestArtifact2' + + - name: ArtifactTarget3 + id: ArtifactTarget3 + run: dotnet run --project AtomTest/AtomTest.csproj -- ArtifactTarget3 --skip --headless + + ArtifactTarget4: + needs: [ ArtifactTarget2 ] + runs-on: ubuntu-latest + strategy: + matrix: + test-slice: [ Slice1, Slice2 ] + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Retrieve TestArtifact2 + uses: actions/download-artifact@v8 + with: + name: 'TestArtifact2-${{ matrix.test-slice }}' + path: '${{ github.workspace }}/.github/artifacts/TestArtifact2' + + - name: ArtifactTarget4 + id: ArtifactTarget4 + run: dotnet run --project AtomTest/AtomTest.csproj -- ArtifactTarget4 --skip --headless + env: + test-slice: ${{ matrix.test-slice }} + build-slice: ${{ matrix.test-slice }} + diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.CheckoutOptionsBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.CheckoutOptionsBuild_GeneratesWorkflow.verified.txt new file mode 100644 index 00000000..28bacc10 --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.CheckoutOptionsBuild_GeneratesWorkflow.verified.txt @@ -0,0 +1,46 @@ +name: checkoutoption-workflow + +on: + workflow_dispatch: + +permissions: { } + +jobs: + + CheckoutOptionTarget1: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: CheckoutOptionTarget1 + id: CheckoutOptionTarget1 + run: dotnet run --project AtomTest/AtomTest.csproj -- CheckoutOptionTarget1 --skip --headless + + CheckoutOptionTarget2: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + token: some-token + fetch-depth: 0 + lfs: true + submodules: recursive + + - name: CheckoutOptionTarget2 + id: CheckoutOptionTarget2 + run: dotnet run --project AtomTest/AtomTest.csproj -- CheckoutOptionTarget2 --skip --headless + + CheckoutOptionTarget3: + runs-on: ubuntu-latest + steps: + + - name: CheckoutOptionTarget3 + id: CheckoutOptionTarget3 + run: dotnet run --project AtomTest/AtomTest.csproj -- CheckoutOptionTarget3 --skip --headless + diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.CustomArtifactBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.CustomArtifactBuild_GeneratesWorkflow.verified.txt similarity index 59% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.CustomArtifactBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.CustomArtifactBuild_GeneratesWorkflow.verified.txt index 4e9235bf..66e46e2d 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.CustomArtifactBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.CustomArtifactBuild_GeneratesWorkflow.verified.txt @@ -2,11 +2,12 @@ on: pull_request: - branches: - - 'main' + branches: [ main ] + +permissions: { } jobs: - + SetupBuildInfo: runs-on: ubuntu-latest outputs: @@ -15,135 +16,137 @@ jobs: build-version: ${{ steps.SetupBuildInfo.outputs.build-version }} build-timestamp: ${{ steps.SetupBuildInfo.outputs.build-timestamp }} steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - + - name: SetupBuildInfo id: SetupBuildInfo - run: dotnet run --project AtomTest/AtomTest.csproj SetupBuildInfo --skip --headless - + run: dotnet run --project AtomTest/AtomTest.csproj -- SetupBuildInfo --skip --headless + ArtifactTarget1: needs: [ SetupBuildInfo ] runs-on: ubuntu-latest steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - + - name: ArtifactTarget1 id: ArtifactTarget1 - run: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget1 --skip --headless - - - name: StoreArtifact - run: dotnet run --project AtomTest/AtomTest.csproj StoreArtifact --skip --headless + run: dotnet run --project AtomTest/AtomTest.csproj -- ArtifactTarget1 --skip --headless + + - name: Store artifact `TestArtifact1` + run: dotnet run --project AtomTest/AtomTest.csproj -- StoreArtifact --skip --headless env: build-name: ${{ needs.SetupBuildInfo.outputs.build-name }} build-id: ${{ needs.SetupBuildInfo.outputs.build-id }} atom-artifacts: TestArtifact1 - + ArtifactTarget2: needs: [ ArtifactTarget1, SetupBuildInfo ] runs-on: ubuntu-latest steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - - name: RetrieveArtifact - run: dotnet run --project AtomTest/AtomTest.csproj RetrieveArtifact --skip --headless + + - name: Retrieve artifact `TestArtifact1` + run: dotnet run --project AtomTest/AtomTest.csproj -- RetrieveArtifact --skip --headless env: build-name: ${{ needs.SetupBuildInfo.outputs.build-name }} build-id: ${{ needs.SetupBuildInfo.outputs.build-id }} atom-artifacts: TestArtifact1 - + - name: ArtifactTarget2 id: ArtifactTarget2 - run: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget2 --skip --headless - - - name: StoreArtifact - run: dotnet run --project AtomTest/AtomTest.csproj StoreArtifact --skip --headless + run: dotnet run --project AtomTest/AtomTest.csproj -- ArtifactTarget2 --skip --headless + + - name: Store artifact `TestArtifact2` + run: dotnet run --project AtomTest/AtomTest.csproj -- StoreArtifact --skip --headless env: build-name: ${{ needs.SetupBuildInfo.outputs.build-name }} build-id: ${{ needs.SetupBuildInfo.outputs.build-id }} atom-artifacts: TestArtifact2 build-slice: Slice1 - - - name: StoreArtifact - run: dotnet run --project AtomTest/AtomTest.csproj StoreArtifact --skip --headless + + - name: Store artifact `TestArtifact2` + run: dotnet run --project AtomTest/AtomTest.csproj -- StoreArtifact --skip --headless env: build-name: ${{ needs.SetupBuildInfo.outputs.build-name }} build-id: ${{ needs.SetupBuildInfo.outputs.build-id }} atom-artifacts: TestArtifact2 build-slice: Slice2 - + ArtifactTarget3: needs: [ ArtifactTarget1, ArtifactTarget2, SetupBuildInfo ] runs-on: ubuntu-latest steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - - name: RetrieveArtifact - run: dotnet run --project AtomTest/AtomTest.csproj RetrieveArtifact --skip --headless + + - name: Retrieve artifact `TestArtifact1` + run: dotnet run --project AtomTest/AtomTest.csproj -- RetrieveArtifact --skip --headless env: build-name: ${{ needs.SetupBuildInfo.outputs.build-name }} build-id: ${{ needs.SetupBuildInfo.outputs.build-id }} atom-artifacts: TestArtifact1 - - - name: RetrieveArtifact - run: dotnet run --project AtomTest/AtomTest.csproj RetrieveArtifact --skip --headless + + - name: Retrieve artifact `TestArtifact2` + run: dotnet run --project AtomTest/AtomTest.csproj -- RetrieveArtifact --skip --headless env: build-name: ${{ needs.SetupBuildInfo.outputs.build-name }} build-id: ${{ needs.SetupBuildInfo.outputs.build-id }} atom-artifacts: TestArtifact2 build-slice: Slice1 - - - name: RetrieveArtifact - run: dotnet run --project AtomTest/AtomTest.csproj RetrieveArtifact --skip --headless + + - name: Retrieve artifact `TestArtifact2` + run: dotnet run --project AtomTest/AtomTest.csproj -- RetrieveArtifact --skip --headless env: build-name: ${{ needs.SetupBuildInfo.outputs.build-name }} build-id: ${{ needs.SetupBuildInfo.outputs.build-id }} atom-artifacts: TestArtifact2 build-slice: Slice2 - + - name: ArtifactTarget3 id: ArtifactTarget3 - run: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget3 --skip --headless - + run: dotnet run --project AtomTest/AtomTest.csproj -- ArtifactTarget3 --skip --headless + ArtifactTarget4: needs: [ ArtifactTarget2, SetupBuildInfo ] + runs-on: ubuntu-latest strategy: matrix: - slice: [ Slice1, Slice2 ] - runs-on: ubuntu-latest + test-slice: [ Slice1, Slice2 ] steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - - name: RetrieveArtifact - run: dotnet run --project AtomTest/AtomTest.csproj RetrieveArtifact --skip --headless + + - name: Retrieve artifact `TestArtifact2` + run: dotnet run --project AtomTest/AtomTest.csproj -- RetrieveArtifact --skip --headless env: build-name: ${{ needs.SetupBuildInfo.outputs.build-name }} build-id: ${{ needs.SetupBuildInfo.outputs.build-id }} + test-slice: ${{ matrix.test-slice }} + build-slice: ${{ matrix.test-slice }} atom-artifacts: TestArtifact2 - build-slice: ${{ matrix.slice }} - + - name: ArtifactTarget4 id: ArtifactTarget4 - run: dotnet run --project AtomTest/AtomTest.csproj ArtifactTarget4 --skip --headless + run: dotnet run --project AtomTest/AtomTest.csproj -- ArtifactTarget4 --skip --headless env: - slice: ${{ matrix.slice }} - build-slice: ${{ matrix.slice }} + test-slice: ${{ matrix.test-slice }} + build-slice: ${{ matrix.test-slice }} + diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.DependabotBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.DependabotBuild_GeneratesWorkflow.verified.txt new file mode 100644 index 00000000..4cc08dab --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.DependabotBuild_GeneratesWorkflow.verified.txt @@ -0,0 +1,58 @@ +version: 2 + +registries: + registry-1: + type: cargo-registry + url: url1 + +updates: + - package-ecosystem: package-ecosystem-1 + directory: "directory" + schedule: + interval: daily + day: monday + time: "02:00" + timezone: UTC + allow: + - dependency-name: "dep-name" + dependency-type: all + assignees: [ assignee1 ] + commit-message: + prefix: "prefix" + prefix-development: "prefix-dev" + include: scope + cooldown: + default-days: 1 + semver-major-days: 2 + semver-minor-days: 3 + semver-patch-days: 4 + include: [ include1 ] + exclude: [ exclude1 ] + exclude-paths: [ exclude-path1, exclude-path2 ] + groups: + group-1: + applies-to: security-updates + dependency-type: production + patterns: [ "pattern-1", "pattern-2" ] + exclude-patterns: [ "exclude-pattern-1", "exclude-pattern-2" ] + update-types: [ major, minor ] + group-by: dependency-name + ignore: + - dependency-name: "dep-name" + update-types: [ version-update:semver-major ] + versions: [ "1.0.0", "2.0.0" ] + insecure-external-code-execution: allow + labels: [ label1, label2 ] + milestone: 1 + name: "update-deps" + open-pull-requests-limit: 5 + pull-request-branch-name: + separator: "-" + rebase-strategy: auto + registries: "*" + target-branch: main + vendor: true + versioning-strategy: increase + patterns: [ pattern1, pattern2 ] + multi-ecosystem-group: multi-ecosystem-group + diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.DependentBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.DependentBuild_GeneratesWorkflow.verified.txt similarity index 58% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.DependentBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.DependentBuild_GeneratesWorkflow.verified.txt index c35cf367..633566d7 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.DependentBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.DependentBuild_GeneratesWorkflow.verified.txt @@ -2,34 +2,36 @@ on: pull_request: - branches: - - 'main' + branches: [ main ] + +permissions: { } jobs: - + DependentTarget1: runs-on: ubuntu-latest steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - + - name: DependentTarget1 id: DependentTarget1 - run: dotnet run --project AtomTest/AtomTest.csproj DependentTarget1 --skip --headless - + run: dotnet run --project AtomTest/AtomTest.csproj -- DependentTarget1 --skip --headless + DependentTarget2: needs: [ DependentTarget1 ] runs-on: ubuntu-latest steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - + - name: DependentTarget2 id: DependentTarget2 - run: dotnet run --project AtomTest/AtomTest.csproj DependentTarget2 --skip --headless + run: dotnet run --project AtomTest/AtomTest.csproj -- DependentTarget2 --skip --headless + diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.DuplicateDependencyBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.DuplicateDependencyBuild_GeneratesWorkflow.verified.txt similarity index 71% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.DuplicateDependencyBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.DuplicateDependencyBuild_GeneratesWorkflow.verified.txt index b4867080..51e7ca6b 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.DuplicateDependencyBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.DuplicateDependencyBuild_GeneratesWorkflow.verified.txt @@ -3,8 +3,10 @@ on: workflow_dispatch: +permissions: { } + jobs: - + SetupBuildInfo: runs-on: ubuntu-latest outputs: @@ -13,36 +15,37 @@ jobs: build-version: ${{ steps.SetupBuildInfo.outputs.build-version }} build-timestamp: ${{ steps.SetupBuildInfo.outputs.build-timestamp }} steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - + - name: SetupBuildInfo id: SetupBuildInfo - run: dotnet run --project AtomTest/AtomTest.csproj SetupBuildInfo --skip --headless - + run: dotnet run --project AtomTest/AtomTest.csproj -- SetupBuildInfo --skip --headless + DuplicateDependencyTarget1: needs: [ SetupBuildInfo ] runs-on: ubuntu-latest steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - + - name: DuplicateDependencyTarget1 id: DuplicateDependencyTarget1 - run: dotnet run --project AtomTest/AtomTest.csproj DuplicateDependencyTarget1 --skip --headless + run: dotnet run --project AtomTest/AtomTest.csproj -- DuplicateDependencyTarget1 --skip --headless env: build-name: ${{ needs.SetupBuildInfo.outputs.build-name }} build-id: ${{ needs.SetupBuildInfo.outputs.build-id }} - - - name: StoreArtifact - run: dotnet run --project AtomTest/AtomTest.csproj StoreArtifact --skip --headless + + - name: Store artifact `artifact-name` + run: dotnet run --project AtomTest/AtomTest.csproj -- StoreArtifact --skip --headless env: build-name: ${{ needs.SetupBuildInfo.outputs.build-name }} build-id: ${{ needs.SetupBuildInfo.outputs.build-id }} atom-artifacts: artifact-name + diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.EnvironmentBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.EnvironmentBuild_GeneratesWorkflow.verified.txt similarity index 64% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.EnvironmentBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.EnvironmentBuild_GeneratesWorkflow.verified.txt index 2a4aa605..d5882b2b 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.EnvironmentBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.EnvironmentBuild_GeneratesWorkflow.verified.txt @@ -3,18 +3,21 @@ on: workflow_dispatch: +permissions: { } + jobs: - + EnvironmentTarget: runs-on: ubuntu-latest environment: test-env-1 steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - + - name: EnvironmentTarget id: EnvironmentTarget - run: dotnet run --project AtomTest/AtomTest.csproj EnvironmentTarget --skip --headless + run: dotnet run --project AtomTest/AtomTest.csproj -- EnvironmentTarget --skip --headless + diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.ManualInputBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.ManualInputBuild_GeneratesWorkflow.verified.txt similarity index 76% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.ManualInputBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.ManualInputBuild_GeneratesWorkflow.verified.txt index d744bb3b..0e7ab006 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.ManualInputBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.ManualInputBuild_GeneratesWorkflow.verified.txt @@ -10,8 +10,8 @@ on: string-param-with-default: description: String param required: false - type: string default: default-value + type: string bool-param-without-default: description: Bool param required: true @@ -19,37 +19,34 @@ on: bool-param-with-default: description: Bool param required: false - type: boolean default: true + type: boolean choice-param-without-default: description: Choice param required: true type: choice - options: - - choice 1 - - choice 2 - - choice 3 + options: [ choice 1, choice 2, choice 3 ] choice-param-with-default: description: Choice param required: false - type: choice - options: - - choice 1 - - choice 2 - - choice 3 default: choice 1 + type: choice + options: [ choice 1, choice 2, choice 3 ] + +permissions: { } jobs: - + ManualInputTarget: runs-on: ubuntu-latest steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - + - name: ManualInputTarget id: ManualInputTarget - run: dotnet run --project AtomTest/AtomTest.csproj ManualInputTarget --skip --headless + run: dotnet run --project AtomTest/AtomTest.csproj -- ManualInputTarget --skip --headless + diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.PermissionsBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.PermissionsBuild_GeneratesWorkflow.verified.txt similarity index 63% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.PermissionsBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.PermissionsBuild_GeneratesWorkflow.verified.txt index 90cb4dec..a698e64c 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.PermissionsBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.PermissionsBuild_GeneratesWorkflow.verified.txt @@ -1,24 +1,25 @@ name: permissions-workflow -permissions: read-all on: pull_request: - branches: - - 'main' + branches: [ main ] + +permissions: read-all jobs: - + PermissionsTarget: - runs-on: ubuntu-latest permissions: actions: write + runs-on: ubuntu-latest steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - + - name: PermissionsTarget id: PermissionsTarget - run: dotnet run --project AtomTest/AtomTest.csproj PermissionsTarget --skip --headless + run: dotnet run --project AtomTest/AtomTest.csproj -- PermissionsTarget --skip --headless + diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.ReleaseTriggerBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.ReleaseTriggerBuild_GeneratesWorkflow.verified.txt similarity index 63% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.ReleaseTriggerBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.ReleaseTriggerBuild_GeneratesWorkflow.verified.txt index a7f06b71..7d2a60d3 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.ReleaseTriggerBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.ReleaseTriggerBuild_GeneratesWorkflow.verified.txt @@ -3,19 +3,21 @@ on: release: types: [ released ] - + +permissions: { } jobs: - + ReleaseTriggerTarget: runs-on: ubuntu-latest steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - + - name: ReleaseTriggerTarget id: ReleaseTriggerTarget - run: dotnet run --project AtomTest/AtomTest.csproj ReleaseTriggerTarget --skip --headless + run: dotnet run --project AtomTest/AtomTest.csproj -- ReleaseTriggerTarget --skip --headless + diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.SetupDotnetBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.SetupDotnetBuild_GeneratesWorkflow.verified.txt new file mode 100644 index 00000000..74ad1052 --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.SetupDotnetBuild_GeneratesWorkflow.verified.txt @@ -0,0 +1,28 @@ +name: setup-dotnet + +on: + push: + branches: [ main ] + +permissions: { } + +jobs: + + SetupDotnetTarget: + runs-on: ubuntu-latest + steps: + + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup .NET 9.0.x + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 9.0.x + + - name: SetupDotnetTarget + id: SetupDotnetTarget + run: dotnet run --project AtomTest/AtomTest.csproj -- SetupDotnetTarget --skip --headless + diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.SimpleBuild_GeneratesWorkflow.verified.txt b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.SimpleBuild_GeneratesWorkflow.verified.txt similarity index 56% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.SimpleBuild_GeneratesWorkflow.verified.txt rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.SimpleBuild_GeneratesWorkflow.verified.txt index 06e7e7f7..db89bddd 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.SimpleBuild_GeneratesWorkflow.verified.txt +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.SimpleBuild_GeneratesWorkflow.verified.txt @@ -2,20 +2,22 @@ on: pull_request: - branches: - - 'main' + branches: [ main ] + +permissions: { } jobs: - + SimpleTarget: runs-on: ubuntu-latest steps: - + - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - + - name: SimpleTarget id: SimpleTarget - run: dotnet run --project AtomTest/AtomTest.csproj SimpleTarget --skip --headless + run: dotnet run --project AtomTest/AtomTest.csproj -- SimpleTarget --skip --headless + diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.cs similarity index 78% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.cs rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.cs index 090ca28f..508dc8d7 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/WorkflowTests.cs +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/GithubWorkflowTests.cs @@ -1,7 +1,9 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; +using Environment = System.Environment; + +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; [TestFixture] -public class WorkflowTests +internal sealed class GithubWorkflowTests { private static string WorkflowDir => Environment.OSVersion.Platform is PlatformID.Win32NT @@ -14,11 +16,13 @@ Environment.OSVersion.Platform is PlatformID.Win32NT : "/Atom/.github/"; [Test] - public void MinimalBuild_GeneratesNoWorkflows() + public void EmptyBuild_GeneratesNoWorkflows() { // Arrange var fileSystem = FileSystemUtils.DefaultMockFileSystem; - var build = CreateTestHost(fileSystem: fileSystem, commandLineArgs: new(true, [new GenArg()])); + + var build = CreateTestHost(fileSystem: fileSystem, + commandLineArgs: new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))])); // Act build.Run(); @@ -36,7 +40,9 @@ public async Task SimpleBuild_GeneratesWorkflow() { // Arrange var fileSystem = FileSystemUtils.DefaultMockFileSystem; - var build = CreateTestHost(fileSystem: fileSystem, commandLineArgs: new(true, [new GenArg()])); + + var build = CreateTestHost(fileSystem: fileSystem, + commandLineArgs: new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))])); // Act await build.RunAsync(); @@ -59,7 +65,9 @@ public async Task DependentBuild_GeneratesWorkflow() { // Arrange var fileSystem = FileSystemUtils.DefaultMockFileSystem; - var build = CreateTestHost(fileSystem: fileSystem, commandLineArgs: new(true, [new GenArg()])); + + var build = CreateTestHost(fileSystem: fileSystem, + commandLineArgs: new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))])); // Act await build.RunAsync(); @@ -83,7 +91,10 @@ public async Task ArtifactBuild_GeneratesWorkflow() // Arrange var fileSystem = FileSystemUtils.DefaultMockFileSystem; var console = new TestConsole(); - var build = CreateTestHost(console, fileSystem, new(true, [new GenArg()])); + + var build = CreateTestHost(console, + fileSystem, + new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))])); // Act await build.RunAsync(); @@ -110,7 +121,7 @@ public async Task CustomArtifactBuild_GeneratesWorkflow() var build = CreateTestHost(console, fileSystem, - new(true, [new GenArg()]), + new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))]), configure: builder => builder.Services.AddSingleton()); // Act @@ -136,7 +147,7 @@ public async Task ManualInputBuild_GeneratesWorkflow() var fileSystem = FileSystemUtils.DefaultMockFileSystem; var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); + commandLineArgs: new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))])); // Act await build.RunAsync(); @@ -163,7 +174,7 @@ public async Task ManualInputStabilityBuild_GeneratesWorkflowWithStableInputs() var build1 = CreateTestHost(fileSystem: fileSystem, commandLineArgs: new(true, [ - new GenArg(), + new CommandArg(nameof(IWorkflowBuildDefinition.Gen)), new ParamArg("--string-param-without-default", nameof(IManualInputStabilityTarget.StringParamWithoutDefault), "1"), @@ -187,7 +198,7 @@ public async Task ManualInputStabilityBuild_GeneratesWorkflowWithStableInputs() var build2 = CreateTestHost(fileSystem: fileSystem, commandLineArgs: new(true, [ - new GenArg(), + new CommandArg(nameof(IWorkflowBuildDefinition.Gen)), new ParamArg("--string-param-without-default", nameof(IManualInputStabilityTarget.StringParamWithoutDefault), "2"), @@ -226,7 +237,7 @@ public async Task SetupDotnetBuild_GeneratesWorkflow() var fileSystem = FileSystemUtils.DefaultMockFileSystem; var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); + commandLineArgs: new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))])); // Act await build.RunAsync(); @@ -251,7 +262,7 @@ public async Task ReleaseTriggerBuild_GeneratesWorkflow() var fileSystem = FileSystemUtils.DefaultMockFileSystem; var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); + commandLineArgs: new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))])); // Act await build.RunAsync(); @@ -276,7 +287,7 @@ public async Task EnvironmentBuild_GeneratesWorkflow() var fileSystem = FileSystemUtils.DefaultMockFileSystem; var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); + commandLineArgs: new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))])); // Act await build.RunAsync(); @@ -294,29 +305,6 @@ public async Task EnvironmentBuild_GeneratesWorkflow() await TestContext.Out.WriteAsync(workflow); } - [Test] - public async Task GithubIfBuild_GeneratesWorkflow() - { - // Arrange - var fileSystem = FileSystemUtils.DefaultMockFileSystem; - var build = CreateTestHost(fileSystem: fileSystem, commandLineArgs: new(true, [new GenArg()])); - - // Act - await build.RunAsync(); - - // Assert - fileSystem - .DirectoryInfo - .New(WorkflowDir) - .Exists - .ShouldBeTrue(); - - var workflow = await fileSystem.File.ReadAllTextAsync($"{WorkflowDir}githubif-workflow.yml"); - - await Verify(workflow); - await TestContext.Out.WriteAsync(workflow); - } - [Test] public async Task CheckoutOptionsBuild_GeneratesWorkflow() { @@ -324,7 +312,7 @@ public async Task CheckoutOptionsBuild_GeneratesWorkflow() var fileSystem = FileSystemUtils.DefaultMockFileSystem; var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); + commandLineArgs: new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))])); // Act await build.RunAsync(); @@ -342,31 +330,6 @@ public async Task CheckoutOptionsBuild_GeneratesWorkflow() await TestContext.Out.WriteAsync(workflow); } - [Test] - public async Task SnapshotImageBuild_GeneratesWorkflow() - { - // Arrange - var fileSystem = FileSystemUtils.DefaultMockFileSystem; - - var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); - - // Act - await build.RunAsync(); - - // Assert - fileSystem - .DirectoryInfo - .New(WorkflowDir) - .Exists - .ShouldBeTrue(); - - var workflow = await fileSystem.File.ReadAllTextAsync($"{WorkflowDir}snapshotimageoption-workflow.yml"); - - await Verify(workflow); - await TestContext.Out.WriteAsync(workflow); - } - [Test] public async Task DuplicateDependencyBuild_GeneratesWorkflow() { @@ -374,7 +337,7 @@ public async Task DuplicateDependencyBuild_GeneratesWorkflow() var fileSystem = FileSystemUtils.DefaultMockFileSystem; var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); + commandLineArgs: new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))])); // Act await build.RunAsync(); @@ -399,7 +362,7 @@ public async Task PermissionsBuild_GeneratesWorkflow() var fileSystem = FileSystemUtils.DefaultMockFileSystem; var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); + commandLineArgs: new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))])); // Act await build.RunAsync(); @@ -417,38 +380,14 @@ public async Task PermissionsBuild_GeneratesWorkflow() await TestContext.Out.WriteAsync(workflow); } - [Test] - public async Task GithubCustomStepBuild_GeneratesWorkflow() - { - // Arrange - var fileSystem = FileSystemUtils.DefaultMockFileSystem; - - var build = CreateTestHost(fileSystem: fileSystem, - commandLineArgs: new(true, [new GenArg()])); - - // Act - await build.RunAsync(); - - // Assert - fileSystem - .DirectoryInfo - .New(WorkflowDir) - .Exists - .ShouldBeTrue(); - - var workflow = await fileSystem.File.ReadAllTextAsync($"{WorkflowDir}github-custom-step-workflow.yml"); - - await Verify(workflow); - await TestContext.Out.WriteAsync(workflow); - } - [Test] public async Task DependabotBuild_GeneratesWorkflow() { // Arrange var fileSystem = FileSystemUtils.DefaultMockFileSystem; - var build = CreateTestHost(fileSystem: fileSystem, commandLineArgs: new(true, [new GenArg()])); + var build = CreateTestHost(fileSystem: fileSystem, + commandLineArgs: new(true, [new CommandArg(nameof(IWorkflowBuildDefinition.Gen))])); // Act await build.RunAsync(); diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/ManualInputBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ManualInputBuild.cs similarity index 58% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/ManualInputBuild.cs rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ManualInputBuild.cs index b16ef495..ab2cbc35 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/ManualInputBuild.cs +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ManualInputBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Tests.Workflows; +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; [BuildDefinition] -public partial class ManualInputBuild : MinimalBuildDefinition, IDevopsWorkflows, IManualInputTarget +public partial class ManualInputBuild : WorkflowBuildDefinition, IGithubWorkflows, IManualInputTarget { public override IReadOnlyList Workflows => [ @@ -13,19 +13,24 @@ public partial class ManualInputBuild : MinimalBuildDefinition, IDevopsWorkflows { Inputs = [ - ManualStringInput.ForParam(ParamDefinitions[Params.StringParamWithoutDefault]), - ManualStringInput.ForParam(ParamDefinitions[Params.StringParamWithDefault]), - ManualBoolInput.ForParam(ParamDefinitions[Params.BoolParamWithoutDefault]), - ManualBoolInput.ForParam(ParamDefinitions[Params.BoolParamWithDefault]), - ManualChoiceInput.ForParam(ParamDefinitions[Params.ChoiceParamWithoutDefault], + ManualStringInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.StringParamWithoutDefault)]), + ManualStringInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.StringParamWithDefault)]), + ManualBoolInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.BoolParamWithoutDefault)]), + ManualBoolInput.ForParam(ParamDefinitions[nameof(IManualInputTarget.BoolParamWithDefault)]), + ManualChoiceInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.ChoiceParamWithoutDefault)], ["choice 1", "choice 2", "choice 3"]), - ManualChoiceInput.ForParam(ParamDefinitions[Params.ChoiceParamWithDefault], + ManualChoiceInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.ChoiceParamWithDefault)], ["choice 1", "choice 2", "choice 3"]), ], }, ], - Targets = [WorkflowTargets.ManualInputTarget], - WorkflowTypes = [Devops.WorkflowType], + Targets = [new(nameof(IManualInputTarget.ManualInputTarget))], + Types = [WorkflowTypes.Github.Action], }, ]; } diff --git a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/ManualInputStabilityBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ManualInputStabilityBuild.cs similarity index 56% rename from DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/ManualInputStabilityBuild.cs rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ManualInputStabilityBuild.cs index df41f9b4..68f5c74f 100644 --- a/DecSm.Atom.Module.GithubWorkflows.Tests/Workflows/ManualInputStabilityBuild.cs +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ManualInputStabilityBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Module.GithubWorkflows.Tests.Workflows; +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; [BuildDefinition] -public partial class ManualInputStabilityBuild : BuildDefinition, IGithubWorkflows, IManualInputStabilityTarget +public partial class ManualInputStabilityBuild : WorkflowBuildDefinition, IGithubWorkflows, IManualInputStabilityTarget { public override IReadOnlyList Workflows => [ @@ -13,19 +13,24 @@ public partial class ManualInputStabilityBuild : BuildDefinition, IGithubWorkflo { Inputs = [ - ManualStringInput.ForParam(Params.StringParamWithoutDefault), - ManualStringInput.ForParam(Params.StringParamWithDefault), - ManualBoolInput.ForParam(Params.BoolParamWithoutDefault), - ManualBoolInput.ForParam(Params.BoolParamWithDefault), - ManualChoiceInput.ForParam(Params.ChoiceParamWithoutDefault, + ManualStringInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.StringParamWithoutDefault)]), + ManualStringInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.StringParamWithDefault)]), + ManualBoolInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.BoolParamWithoutDefault)]), + ManualBoolInput.ForParam(ParamDefinitions[nameof(IManualInputTarget.BoolParamWithDefault)]), + ManualChoiceInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.ChoiceParamWithoutDefault)], ["choice 1", "choice 2", "choice 3"]), - ManualChoiceInput.ForParam(Params.ChoiceParamWithDefault, + ManualChoiceInput.ForParam( + ParamDefinitions[nameof(IManualInputTarget.ChoiceParamWithDefault)], ["choice 1", "choice 2", "choice 3"]), ], }, ], - Targets = [WorkflowTargets.ManualInputTarget], - WorkflowTypes = [Github.WorkflowType], + Targets = [new(nameof(IManualInputStabilityTarget.ManualInputTarget))], + Types = [WorkflowTypes.Github.Action], }, ]; } diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/PermissionsBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/PermissionsBuild.cs new file mode 100644 index 00000000..ea14886e --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/PermissionsBuild.cs @@ -0,0 +1,39 @@ +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class PermissionsBuild : WorkflowBuildDefinition, IGithubWorkflows, IPermissionsTarget +{ + public override IReadOnlyList Workflows => + [ + new("permissions-workflow") + { + Triggers = + [ + new GitPullRequestTrigger + { + IncludedBranches = ["main"], + }, + ], + Targets = + [ + new(nameof(IPermissionsTarget.PermissionsTarget)) + { + Options = + [ + new GithubTokenPermissionsOption(new PermissionsEvent + { + Actions = PermissionsLevel.Write, + }), + ], + }, + ], + Types = [WorkflowTypes.Github.Action], + Options = [BuildOptions.Github.TokenPermissions.ReadAll], + }, + ]; +} + +public interface IPermissionsTarget +{ + Target PermissionsTarget => t => t; +} diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ReleaseTriggerBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ReleaseTriggerBuild.cs new file mode 100644 index 00000000..5f416e60 --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/ReleaseTriggerBuild.cs @@ -0,0 +1,20 @@ +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class ReleaseTriggerBuild : WorkflowBuildDefinition, IGithubWorkflows, IReleaseTriggerTarget +{ + public override IReadOnlyList Workflows => + [ + new("releasetrigger-workflow") + { + Triggers = [WorkflowTriggers.Github.OnRelease()], + Targets = [new(nameof(IReleaseTriggerTarget.ReleaseTriggerTarget))], + Types = [WorkflowTypes.Github.Action], + }, + ]; +} + +public interface IReleaseTriggerTarget +{ + Target ReleaseTriggerTarget => t => t; +} diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/SetupDotnetBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/SetupDotnetBuild.cs new file mode 100644 index 00000000..d77f69a9 --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/SetupDotnetBuild.cs @@ -0,0 +1,26 @@ +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; + +[BuildDefinition] +public partial class SetupDotnetBuild : WorkflowBuildDefinition, IGithubWorkflows, ISetupDotnetTarget +{ + public override IReadOnlyList Workflows => + [ + new("setup-dotnet") + { + Triggers = [WorkflowTriggers.PushToMain], + Targets = + [ + new(nameof(ISetupDotnetTarget.SetupDotnetTarget)) + { + Options = [BuildOptions.Steps.SetupDotnet.Dotnet90X()], + }, + ], + Types = [WorkflowTypes.Github.Action], + }, + ]; +} + +public interface ISetupDotnetTarget +{ + Target SetupDotnetTarget => t => t; +} diff --git a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/SimpleBuild.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/SimpleBuild.cs similarity index 59% rename from DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/SimpleBuild.cs rename to tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/SimpleBuild.cs index 4d959614..382c0525 100644 --- a/DecSm.Atom.Module.DevopsWorkflows.Tests/Workflows/SimpleBuild.cs +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/Workflows/SimpleBuild.cs @@ -1,7 +1,7 @@ -namespace DecSm.Atom.Module.DevopsWorkflows.Tests.Workflows; +namespace Invex.Atom.Module.GithubWorkflows.Tests.Workflows; [BuildDefinition] -public partial class SimpleBuild : MinimalBuildDefinition, IDevopsWorkflows, ISimpleTarget +public partial class SimpleBuild : WorkflowBuildDefinition, IGithubWorkflows, ISimpleTarget { public override IReadOnlyList Workflows => [ @@ -14,8 +14,8 @@ public partial class SimpleBuild : MinimalBuildDefinition, IDevopsWorkflows, ISi IncludedBranches = ["main"], }, ], - Targets = [WorkflowTargets.SimpleTarget], - WorkflowTypes = [Devops.WorkflowType], + Targets = [new(nameof(ISimpleTarget.SimpleTarget))], + Types = [WorkflowTypes.Github.Action], }, ]; } diff --git a/tests/Invex.Atom.Module.GithubWorkflows.Tests/_usings.cs b/tests/Invex.Atom.Module.GithubWorkflows.Tests/_usings.cs new file mode 100644 index 00000000..8e8c3bf3 --- /dev/null +++ b/tests/Invex.Atom.Module.GithubWorkflows.Tests/_usings.cs @@ -0,0 +1,23 @@ +global using Invex.Atom.Build; +global using Invex.Atom.Build.Args; +global using Invex.Atom.Build.Artifacts; +global using Invex.Atom.Build.BuildOptions; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Build.Params; +global using Invex.Atom.Module.GithubWorkflows.Extensions; +global using Invex.Atom.Module.GithubWorkflows.Helpers; +global using Invex.Atom.Module.GithubWorkflows.Options; +global using Invex.Atom.TestUtils; +global using Invex.Atom.Workflows; +global using Invex.Atom.Workflows.Definition; +global using Invex.Atom.Workflows.Definition.Triggers; +global using Invex.Atom.Workflows.Options; +global using Invex.StructuredText.GithubActions.DependabotConfigModel.Model; +global using Invex.StructuredText.GithubActions.GithubActionModel; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Shouldly; +global using Spectre.Console.Testing; +global using static Invex.Atom.TestUtils.TestUtils; +global using Target = Invex.Atom.Build.Definition.Target; diff --git a/DecSm.Atom.TestUtils/ConsoleOutputUtils.cs b/tests/Invex.Atom.TestUtils/ConsoleOutputUtils.cs similarity index 93% rename from DecSm.Atom.TestUtils/ConsoleOutputUtils.cs rename to tests/Invex.Atom.TestUtils/ConsoleOutputUtils.cs index ed18d377..fda47b91 100644 --- a/DecSm.Atom.TestUtils/ConsoleOutputUtils.cs +++ b/tests/Invex.Atom.TestUtils/ConsoleOutputUtils.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.TestUtils; +namespace Invex.Atom.TestUtils; [PublicAPI] public static partial class ConsoleOutputUtils diff --git a/DecSm.Atom.TestUtils/FileSystemUtils.cs b/tests/Invex.Atom.TestUtils/FileSystemUtils.cs similarity index 95% rename from DecSm.Atom.TestUtils/FileSystemUtils.cs rename to tests/Invex.Atom.TestUtils/FileSystemUtils.cs index 470a19aa..fe4d71fa 100644 --- a/DecSm.Atom.TestUtils/FileSystemUtils.cs +++ b/tests/Invex.Atom.TestUtils/FileSystemUtils.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.TestUtils; +namespace Invex.Atom.TestUtils; public static class FileSystemUtils { diff --git a/DecSm.Atom.TestUtils/DecSm.Atom.TestUtils.csproj b/tests/Invex.Atom.TestUtils/Invex.Atom.TestUtils.csproj similarity index 92% rename from DecSm.Atom.TestUtils/DecSm.Atom.TestUtils.csproj rename to tests/Invex.Atom.TestUtils/Invex.Atom.TestUtils.csproj index 47e3603e..612c1edb 100644 --- a/DecSm.Atom.TestUtils/DecSm.Atom.TestUtils.csproj +++ b/tests/Invex.Atom.TestUtils/Invex.Atom.TestUtils.csproj @@ -5,12 +5,12 @@ - + + - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -26,7 +26,7 @@ - + diff --git a/DecSm.Atom.TestUtils/TestArtifactProvider.cs b/tests/Invex.Atom.TestUtils/TestArtifactProvider.cs similarity index 77% rename from DecSm.Atom.TestUtils/TestArtifactProvider.cs rename to tests/Invex.Atom.TestUtils/TestArtifactProvider.cs index 87082925..d09c26f7 100644 --- a/DecSm.Atom.TestUtils/TestArtifactProvider.cs +++ b/tests/Invex.Atom.TestUtils/TestArtifactProvider.cs @@ -1,23 +1,23 @@ -namespace DecSm.Atom.TestUtils; +namespace Invex.Atom.TestUtils; [PublicAPI] public sealed class TestArtifactProvider : IArtifactProvider { public Task StoreArtifacts( - IReadOnlyList artifactNames, + IEnumerable artifactNames, string? buildId = null, string? buildSlice = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public Task RetrieveArtifacts( - IReadOnlyList artifactNames, + IEnumerable artifactNames, string? buildId = null, string? buildSlice = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task Cleanup(IReadOnlyList runIdentifiers, CancellationToken cancellationToken = default) => + public Task Cleanup(IEnumerable runIdentifiers, CancellationToken cancellationToken = default) => throw new NotImplementedException(); public Task> GetStoredRunIdentifiers( @@ -28,7 +28,7 @@ public Task> GetStoredRunIdentifiers( public Task RetrieveArtifact( string artifactName, - IReadOnlyList buildIds, + IEnumerable buildIds, string? buildSlice = null, CancellationToken cancellationToken = default) => throw new NotImplementedException(); diff --git a/DecSm.Atom.TestUtils/TestBuildIdProvider.cs b/tests/Invex.Atom.TestUtils/TestBuildIdProvider.cs similarity index 77% rename from DecSm.Atom.TestUtils/TestBuildIdProvider.cs rename to tests/Invex.Atom.TestUtils/TestBuildIdProvider.cs index dbc3920d..03454765 100644 --- a/DecSm.Atom.TestUtils/TestBuildIdProvider.cs +++ b/tests/Invex.Atom.TestUtils/TestBuildIdProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.TestUtils; +namespace Invex.Atom.TestUtils; [PublicAPI] public class TestBuildIdProvider : IBuildIdProvider diff --git a/DecSm.Atom.TestUtils/TestBuildTimestampProvider.cs b/tests/Invex.Atom.TestUtils/TestBuildTimestampProvider.cs similarity index 83% rename from DecSm.Atom.TestUtils/TestBuildTimestampProvider.cs rename to tests/Invex.Atom.TestUtils/TestBuildTimestampProvider.cs index c8a585d2..1915f41b 100644 --- a/DecSm.Atom.TestUtils/TestBuildTimestampProvider.cs +++ b/tests/Invex.Atom.TestUtils/TestBuildTimestampProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.TestUtils; +namespace Invex.Atom.TestUtils; [PublicAPI] public sealed class TestBuildTimestampProvider : IBuildTimestampProvider diff --git a/DecSm.Atom.TestUtils/TestBuildVersionProvider.cs b/tests/Invex.Atom.TestUtils/TestBuildVersionProvider.cs similarity index 80% rename from DecSm.Atom.TestUtils/TestBuildVersionProvider.cs rename to tests/Invex.Atom.TestUtils/TestBuildVersionProvider.cs index 22663da7..25dddaf6 100644 --- a/DecSm.Atom.TestUtils/TestBuildVersionProvider.cs +++ b/tests/Invex.Atom.TestUtils/TestBuildVersionProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.TestUtils; +namespace Invex.Atom.TestUtils; [PublicAPI] public class TestBuildVersionProvider : IBuildVersionProvider diff --git a/DecSm.Atom.TestUtils/TestLogger.cs b/tests/Invex.Atom.TestUtils/TestLogger.cs similarity index 93% rename from DecSm.Atom.TestUtils/TestLogger.cs rename to tests/Invex.Atom.TestUtils/TestLogger.cs index 1305bb15..3d9c4ec3 100644 --- a/DecSm.Atom.TestUtils/TestLogger.cs +++ b/tests/Invex.Atom.TestUtils/TestLogger.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.TestUtils; +namespace Invex.Atom.TestUtils; [PublicAPI] public sealed class TestLogger : ILogger diff --git a/DecSm.Atom.TestUtils/TestLoggerProvider.cs b/tests/Invex.Atom.TestUtils/TestLoggerProvider.cs similarity index 86% rename from DecSm.Atom.TestUtils/TestLoggerProvider.cs rename to tests/Invex.Atom.TestUtils/TestLoggerProvider.cs index cf5ac7fc..59ff4e29 100644 --- a/DecSm.Atom.TestUtils/TestLoggerProvider.cs +++ b/tests/Invex.Atom.TestUtils/TestLoggerProvider.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.TestUtils; +namespace Invex.Atom.TestUtils; [PublicAPI] public sealed class TestLoggerProvider : ILoggerProvider diff --git a/DecSm.Atom.TestUtils/TestUtils.cs b/tests/Invex.Atom.TestUtils/TestUtils.cs similarity index 80% rename from DecSm.Atom.TestUtils/TestUtils.cs rename to tests/Invex.Atom.TestUtils/TestUtils.cs index 2aaa357d..fecfb170 100644 --- a/DecSm.Atom.TestUtils/TestUtils.cs +++ b/tests/Invex.Atom.TestUtils/TestUtils.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.TestUtils; +namespace Invex.Atom.TestUtils; [PublicAPI] public static class TestUtils @@ -10,7 +10,7 @@ public static IHost CreateTestHost( TestBuildIdProvider? buildIdProvider = null, TestBuildVersionProvider? buildVersionProvider = null, Action? configure = null) - where T : MinimalBuildDefinition + where T : BuildDefinition { var builder = AtomHost.CreateAtomBuilder([]); @@ -24,14 +24,14 @@ public static IHost CreateTestHost( Args = commandLineArgs .Args .Append(new ProjectArg("AtomTest")) - .ToArray(), + .ToList(), }; buildIdProvider ??= new(); buildVersionProvider ??= new(); - builder.Services.AddKeyedSingleton("StaticAccess", console); - builder.Services.AddKeyedSingleton("RootFileSystem", fileSystem); + builder.Services.AddSingleton(console); + builder.Services.AddKeyedSingleton("RootedFileSystem", fileSystem); builder.Services.AddSingleton(commandLineArgs); builder.Services.AddSingleton(buildIdProvider); builder.Services.AddSingleton(buildVersionProvider); diff --git a/tests/Invex.Atom.TestUtils/TestWorkflowOption.cs b/tests/Invex.Atom.TestUtils/TestWorkflowOption.cs new file mode 100644 index 00000000..d1e43482 --- /dev/null +++ b/tests/Invex.Atom.TestUtils/TestWorkflowOption.cs @@ -0,0 +1,4 @@ +namespace Invex.Atom.TestUtils; + +[PublicAPI] +public sealed record TestWorkflowOption(string Value = "") : IBuildOption; diff --git a/tests/Invex.Atom.TestUtils/_usings.cs b/tests/Invex.Atom.TestUtils/_usings.cs new file mode 100644 index 00000000..35553d33 --- /dev/null +++ b/tests/Invex.Atom.TestUtils/_usings.cs @@ -0,0 +1,17 @@ +global using System.IO.Abstractions; +global using System.IO.Abstractions.TestingHelpers; +global using System.Text; +global using System.Text.RegularExpressions; +global using Invex.Atom.Build.Args; +global using Invex.Atom.Build.Artifacts; +global using Invex.Atom.Build.BuildOptions; +global using Invex.Atom.Build.BuildInfo; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.Hosting; +global using Invex.Atom.Build.Util.Scope; +global using JetBrains.Annotations; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using Spectre.Console; +global using Spectre.Console.Testing; diff --git a/tests/Invex.Atom.Tool.Tests/FileFinderTests.cs b/tests/Invex.Atom.Tool.Tests/FileFinderTests.cs new file mode 100644 index 00000000..8ae48100 --- /dev/null +++ b/tests/Invex.Atom.Tool.Tests/FileFinderTests.cs @@ -0,0 +1,343 @@ +namespace Invex.Atom.Tool.Tests; + +[TestFixture] +internal sealed class FileFinderTests +{ + [SetUp] + public void SetUp() => + _fs = new(); + + private MockFileSystem _fs = null!; + + private static string Root => + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? @"C:\" + : "/"; + + private string CombinePath(params string[] parts) => + _fs.Path.Combine([Root, ..parts]); + + [Test] + public void FindFile_WhenStartDirectoryIsExistingFile_ReturnsImmediately() + { + var filePath = CombinePath("repo", "build.csproj"); + _fs.AddFile(filePath, new("")); + + var result = FileFinder.FindFile(_fs, filePath, ["build.csproj"], false); + + result.ShouldNotBeNull(); + result.FullName.ShouldBe(filePath); + } + + [Test] + public void FindFile_FileInCurrentDirectory_ReturnsFile() + { + var dir = CombinePath("repo"); + var filePath = CombinePath("repo", "_atom.csproj"); + _fs.AddDirectory(dir); + _fs.AddFile(filePath, new("")); + _fs.Directory.SetCurrentDirectory(dir); + + var result = FileFinder.FindFile(_fs, dir, ["_atom.csproj"], false); + + result.ShouldNotBeNull(); + result.FullName.ShouldBe(filePath); + } + + [Test] + public void FindFile_FileNotInCurrentOrParentOrChild_ReturnsNull() + { + var dir = CombinePath("empty"); + _fs.AddDirectory(dir); + _fs.Directory.SetCurrentDirectory(dir); + + var result = FileFinder.FindFile(_fs, dir, ["missing.csproj"], false); + + result.ShouldBeNull(); + } + + [Test] + public void FindFile_FileInParentDirectory_ReturnsFile() + { + var childDir = CombinePath("repo", "src", "project"); + var filePath = CombinePath("repo", "_atom.csproj"); + + _fs.AddDirectory(childDir); + _fs.AddFile(filePath, new("")); + + var result = FileFinder.FindFile(_fs, childDir, ["_atom.csproj"], false); + + result.ShouldNotBeNull(); + result.FullName.ShouldBe(filePath); + } + + [Test] + public void FindFile_StopsUpwardSearchAtGitDirectory() + { + // .git is at the root of subDir — searching from deeper should NOT find + // the project file that lives ABOVE the .git boundary. + var aboveGit = CombinePath("workspace"); + var repoRoot = CombinePath("workspace", "repo"); + var deepDir = CombinePath("workspace", "repo", "src"); + + _fs.AddDirectory(deepDir); + _fs.AddDirectory(_fs.Path.Combine(repoRoot, ".git")); // git root marker + _fs.AddFile(_fs.Path.Combine(aboveGit, "above.csproj"), new("")); + + _fs.Directory.SetCurrentDirectory(deepDir); + + var result = FileFinder.FindFile(_fs, deepDir, ["above.csproj"], false); + + result.ShouldBeNull(); + } + + [Test] + public void FindFile_StopsUpwardSearchAtSlnFile() + { + // FileFinder stops when it finds a file/directory LITERALLY named ".sln" + var workspace = CombinePath("workspace"); + var repo = CombinePath("workspace", "repo"); + var deep = CombinePath("workspace", "repo", "sub"); + + _fs.AddDirectory(deep); + _fs.AddFile(_fs.Path.Combine(repo, ".sln"), new("")); // exact name ".sln" + _fs.AddFile(_fs.Path.Combine(workspace, "above.csproj"), new("")); + + var result = FileFinder.FindFile(_fs, deep, ["above.csproj"], false); + + result.ShouldBeNull(); + } + + [Test] + public void FindFile_StopsUpwardSearchAtSlnxFile() + { + // FileFinder stops when it finds a file/directory LITERALLY named ".slnx" + var workspace = CombinePath("workspace"); + var repo = CombinePath("workspace", "repo"); + var deep = CombinePath("workspace", "repo", "sub"); + + _fs.AddDirectory(deep); + _fs.AddFile(_fs.Path.Combine(repo, ".slnx"), new("")); // exact name ".slnx" + _fs.AddFile(_fs.Path.Combine(workspace, "above.csproj"), new("")); + + var result = FileFinder.FindFile(_fs, deep, ["above.csproj"], false); + + result.ShouldBeNull(); + } + + [Test] + public void FindFile_FindsFileInSameDirectoryAsRootMarker() + { + // File is at the same level as the .git marker (i.e., in the repo root) + var repoRoot = CombinePath("repo"); + var deep = CombinePath("repo", "src"); + + _fs.AddDirectory(deep); + _fs.AddDirectory(_fs.Path.Combine(repoRoot, ".git")); + _fs.AddFile(_fs.Path.Combine(repoRoot, "_atom.csproj"), new("")); + + var result = FileFinder.FindFile(_fs, deep, ["_atom.csproj"], false); + + result.ShouldNotBeNull(); + } + + [Test] + public void FindFile_FileInSubdirectory_ReturnsFile() + { + var baseDir = CombinePath("workspace"); + var filePath = CombinePath("workspace", "atoms", "_atom.csproj"); + + _fs.AddDirectory(baseDir); + _fs.AddFile(filePath, new("")); + + var result = FileFinder.FindFile(_fs, baseDir, ["_atom.csproj"], false); + + result.ShouldNotBeNull(); + result.FullName.ShouldBe(filePath); + } + + [Test] + public void FindFile_DownwardSearch_SkipsBinFolder() + { + var baseDir = CombinePath("workspace"); + var binDir = CombinePath("workspace", "bin"); + _fs.AddDirectory(baseDir); + _fs.AddFile(_fs.Path.Combine(binDir, "_atom.csproj"), new("")); + + var result = FileFinder.FindFile(_fs, baseDir, ["_atom.csproj"], false); + + result.ShouldBeNull(); + } + + [Test] + public void FindFile_DownwardSearch_SkipsObjFolder() + { + var baseDir = CombinePath("workspace"); + _fs.AddDirectory(baseDir); + _fs.AddFile(_fs.Path.Combine(baseDir, "obj", "_atom.csproj"), new("")); + + var result = FileFinder.FindFile(_fs, baseDir, ["_atom.csproj"], false); + + result.ShouldBeNull(); + } + + [Test] + public void FindFile_DownwardSearch_SkipsNodeModulesFolder() + { + var baseDir = CombinePath("workspace"); + _fs.AddDirectory(baseDir); + _fs.AddFile(_fs.Path.Combine(baseDir, "node_modules", "tool.csproj"), new("")); + + var result = FileFinder.FindFile(_fs, baseDir, ["tool.csproj"], false); + + result.ShouldBeNull(); + } + + [Test] + public void FindFile_DownwardSearch_SkipsGitFolder() + { + var baseDir = CombinePath("workspace"); + _fs.AddDirectory(baseDir); + _fs.AddFile(_fs.Path.Combine(baseDir, ".git", "target.csproj"), new("")); + + var result = FileFinder.FindFile(_fs, baseDir, ["target.csproj"], false); + + result.ShouldBeNull(); + } + + [Test] + public void FindFile_DownwardSearch_SkipsVsFolder() + { + var baseDir = CombinePath("workspace"); + _fs.AddDirectory(baseDir); + _fs.AddFile(_fs.Path.Combine(baseDir, ".vs", "target.csproj"), new("")); + + var result = FileFinder.FindFile(_fs, baseDir, ["target.csproj"], false); + + result.ShouldBeNull(); + } + + [Test] + public void FindFile_DownwardSearch_RespectsMaxDepthFourLevels() + { + var baseDir = CombinePath("workspace"); + + // Depth 4 from baseDir + var tooDeepPath = _fs.Path.Combine(baseDir, "L1", "L2", "L3", "L4", "L5", "target.csproj"); + + _fs.AddDirectory(baseDir); + _fs.AddFile(tooDeepPath, new("")); + + var result = FileFinder.FindFile(_fs, baseDir, ["target.csproj"], false); + + result.ShouldBeNull(); + } + + [Test] + public void FindFile_DownwardSearch_FindsAtMaxAllowedDepth() + { + var baseDir = CombinePath("workspace"); + + // Exactly at depth 4 (MaxDownstreamDepth) + var atMaxDepthPath = _fs.Path.Combine(baseDir, "L1", "L2", "L3", "L4", "target.csproj"); + + _fs.AddDirectory(baseDir); + _fs.AddFile(atMaxDepthPath, new("")); + + var result = FileFinder.FindFile(_fs, baseDir, ["target.csproj"], false); + + result.ShouldNotBeNull(); + } + + [Test] + public void FindFile_WithConventionSearch_FindsFileInMatchingSubdirectory() + { + // If searching for "_atom.csproj" and there is a folder named "_atom" + // containing "_atom.csproj", it should be found. + var baseDir = CombinePath("repo"); + var nestedPath = CombinePath("repo", "_atom", "_atom.csproj"); + + _fs.AddDirectory(baseDir); + _fs.AddFile(nestedPath, new("")); + + var result = FileFinder.FindFile(_fs, baseDir, ["_atom.csproj"], true); + + result.ShouldNotBeNull(); + result.FullName.ShouldBe(nestedPath); + } + + [Test] + public void FindFile_WithoutConventionSearch_DoesNotFindFileInMatchingSubdirectory() + { + var baseDir = CombinePath("repo"); + var nestedPath = CombinePath("repo", "_atom", "_atom.csproj"); + + _fs.AddDirectory(baseDir); + _fs.AddFile(nestedPath, new("")); + + // Without convention search, the file at "_atom/_atom.csproj" is not + // found unless we happen to traverse into the "_atom" subfolder during BFS. + // Since the BFS DOES descend into subfolders, it will find it — BUT the + // convention search only applies during the upward walk. + // Testing the *direct* case: just the nested path, no non-nested version. + var result = FileFinder.FindFile(_fs, baseDir, ["_atom.csproj"], false); + + // BFS WILL find it because "_atom" subfolder is not excluded + result.ShouldNotBeNull(); + } + + [Test] + public void FindFile_ConventionSearch_OnlyUsedDuringUpwardWalk() + { + // Convention search: parent directory should match filename without extension. + // Put only a file at /repo/parent1/_atom.csproj where parent1 != _atom + // So no convention match. + var baseDir = CombinePath("repo"); + _fs.AddDirectory(baseDir); + _fs.AddFile(CombinePath("repo", "parent1", "_atom.csproj"), new("")); + + // The upward walk checks /repo for _atom.csproj directly, not /repo/parent1/_atom.csproj + // convention requires parent dir name == filename-without-ext + // parent1 != _atom, so convention doesn't match it + // But BFS downward WILL find it (parent1 is not excluded). + var result = FileFinder.FindFile(_fs, baseDir, ["_atom.csproj"], true); + + // BFS finds it anyway through normal traversal + result.ShouldNotBeNull(); + } + + [Test] + public void FindFile_WithMultipleFileNames_ReturnsFirstMatch() + { + var dir = CombinePath("workspace"); + _fs.AddDirectory(dir); + _fs.AddFile(_fs.Path.Combine(dir, "second.csproj"), new("")); + + var result = FileFinder.FindFile(_fs, dir, ["first.csproj", "second.csproj"], false); + + result.ShouldNotBeNull(); + + _fs + .Path + .GetFileName(result.FullName) + .ShouldBe("second.csproj"); + } + + [Test] + public void FindFile_AllNamesExist_ReturnsFirstNameMatch() + { + var dir = CombinePath("workspace"); + _fs.AddDirectory(dir); + _fs.AddFile(_fs.Path.Combine(dir, "first.csproj"), new("")); + _fs.AddFile(_fs.Path.Combine(dir, "second.csproj"), new("")); + + var result = FileFinder.FindFile(_fs, dir, ["first.csproj", "second.csproj"], false); + + result.ShouldNotBeNull(); + + _fs + .Path + .GetFileName(result.FullName) + .ShouldBe("first.csproj"); + } +} diff --git a/DecSm.Atom.Tool.Tests/DecSm.Atom.Tool.Tests.csproj b/tests/Invex.Atom.Tool.Tests/Invex.Atom.Tool.Tests.csproj similarity index 95% rename from DecSm.Atom.Tool.Tests/DecSm.Atom.Tool.Tests.csproj rename to tests/Invex.Atom.Tool.Tests/Invex.Atom.Tool.Tests.csproj index 6f37abf1..40b6ba8e 100644 --- a/DecSm.Atom.Tool.Tests/DecSm.Atom.Tool.Tests.csproj +++ b/tests/Invex.Atom.Tool.Tests/Invex.Atom.Tool.Tests.csproj @@ -1,4 +1,4 @@ - + net10.0;net9.0;net8.0 @@ -47,7 +47,7 @@ - + diff --git a/tests/Invex.Atom.Tool.Tests/RunArgsFilterTests.cs b/tests/Invex.Atom.Tool.Tests/RunArgsFilterTests.cs new file mode 100644 index 00000000..a5c1b413 --- /dev/null +++ b/tests/Invex.Atom.Tool.Tests/RunArgsFilterTests.cs @@ -0,0 +1,120 @@ +namespace Invex.Atom.Tool.Tests; + +[TestFixture] +internal sealed class RunArgsFilterTests +{ + // A terminal ConsoleAppFilter that captures the context passed to it. + // We pass null! because CapturingFilter IS the terminus — Next is never called. + private sealed class CapturingFilter() : ConsoleAppFilter(null!) + { + public ConsoleAppContext? CapturedContext { get; private set; } + + public override Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) + { + CapturedContext = context; + + return Task.CompletedTask; + } + } + + private static ConsoleAppContext MakeContext(params string[] args) => + // Use positional construction — parameter names differ across generator versions. + // Signature: (commandName, arguments, rawArguments/runArguments, state, methodName?, commandDepth, escapeIndex) + new("atom", args, ReadOnlyMemory.Empty, null, null, 0, 0); + + private static async Task InvokeFilter(string[] args) + { + var capture = new CapturingFilter(); + var filter = new RunArgsFilter(capture); + await filter.InvokeAsync(MakeContext(args), CancellationToken.None); + + return capture.CapturedContext; + } + + [Test] + public async Task InvokeAsync_NoArguments_PassesContextUnchanged() + { + var ctx = await InvokeFilter([]); + + ctx.ShouldNotBeNull(); + ctx.Arguments.ShouldBeEmpty(); + } + + [Test] + public async Task InvokeAsync_NugetAddCommand_PassesContextWithoutChangingEscapeIndex() + { + // nuget-add should be passed through with default EscapeIndex (null / 0) + var ctx = await InvokeFilter(["nuget-add", "my-feed", "https://example.com"]); + + ctx.ShouldNotBeNull(); + ctx.Arguments.ShouldBe(["nuget-add", "my-feed", "https://example.com"]); + } + + [Test] + public async Task InvokeAsync_DirectAtomArgs_SetsEscapeIndexToZero() + { + // Args like ["Build", "--verbose"] don't start with -p/--project + // → all args should be forwarded to Atom → EscapeIndex = 0 + var ctx = await InvokeFilter(["Build", "--verbose"]); + + ctx.ShouldNotBeNull(); + ctx.EscapeIndex.ShouldBe(0); + } + + [Test] + public async Task InvokeAsync_ShortProjectFlag_SetsEscapeIndexToTwo() + { + // atom -p _atom Build → EscapeIndex should be 2 (skip "-p" and "_atom") + var ctx = await InvokeFilter(["-p", "_atom", "Build"]); + + ctx.ShouldNotBeNull(); + ctx.EscapeIndex.ShouldBe(2); + } + + [Test] + public async Task InvokeAsync_LongProjectFlag_SetsEscapeIndexToTwo() + { + var ctx = await InvokeFilter(["--project", "_atom", "Build"]); + + ctx.ShouldNotBeNull(); + ctx.EscapeIndex.ShouldBe(2); + } + + [Test] + public async Task InvokeAsync_ShortFileFlag_SetsEscapeIndexToTwo() + { + var ctx = await InvokeFilter(["-f", "build.cs", "Build"]); + + ctx.ShouldNotBeNull(); + ctx.EscapeIndex.ShouldBe(2); + } + + [Test] + public async Task InvokeAsync_LongFileFlag_SetsEscapeIndexToTwo() + { + var ctx = await InvokeFilter(["--file", "build.cs", "Build"]); + + ctx.ShouldNotBeNull(); + ctx.EscapeIndex.ShouldBe(2); + } + + [Test] + public async Task InvokeAsync_ProjectFlagWithNoValue_SetsEscapeIndexToZero() + { + // "-p" alone (Length < 2): context.Arguments.Length is 1, which is NOT >= 2 + // so EscapeIndex should fall through to 0 + var ctx = await InvokeFilter(["-p"]); + + ctx.ShouldNotBeNull(); + ctx.EscapeIndex.ShouldBe(0); + } + + [Test] + public async Task InvokeAsync_ProjectFlag_ArgumentsArePreserved() + { + var ctx = await InvokeFilter(["--project", "myproj", "Build", "--no-restore"]); + + ctx.ShouldNotBeNull(); + ctx.Arguments.ShouldBe(["--project", "myproj", "Build", "--no-restore"]); + } +} diff --git a/tests/Invex.Atom.Tool.Tests/RunCommandTests.cs b/tests/Invex.Atom.Tool.Tests/RunCommandTests.cs new file mode 100644 index 00000000..c9e4aaa3 --- /dev/null +++ b/tests/Invex.Atom.Tool.Tests/RunCommandTests.cs @@ -0,0 +1,499 @@ +namespace Invex.Atom.Tool.Tests; + +[TestFixture] +internal sealed class RunCommandTests +{ + [SetUp] + public void SetUp() + { + _fs = new(); + RunCommand.FileSystem = _fs; + RunCommand.MockDotnetCli = true; + Environment.SetEnvironmentVariable("ATOM_NO_RESTORE_CACHE", null); + } + + [TearDown] + public void TearDown() + { + RunCommand.FileSystem = new FileSystem(); + RunCommand.MockDotnetCli = false; + Environment.SetEnvironmentVariable("ATOM_NO_RESTORE_CACHE", null); + } + + private MockFileSystem _fs = null!; + + private static string Root => + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? @"C:\" + : "/"; + + private string P(params string[] parts) => + _fs.Path.Combine([Root, ..parts]); + + [Test] + public async Task Handle_ShouldFindProjectInParent_AndStopAtRootMarker() + { + var subDir = P("Repo", "SubFolder"); + var targetDir = P("Repo", "SubFolder", "Target"); + + _fs.AddDirectory(targetDir); + _fs.AddFile(P("Repo", "MyProj.csproj"), new("")); + _fs.AddDirectory(_fs.Path.Combine(subDir, ".git")); // root marker + + _fs.Directory.SetCurrentDirectory(targetDir); + + var result = await RunCommand.Handle([], "MyProj", CancellationToken.None); + + result.ShouldBe(1); // blocked by .git root marker + } + + [Test] + public async Task Handle_ShouldFindNestedProject_WhenConventionSearchIsEnabled() + { + var workDir = P("Work"); + _fs.AddFile(P("Work", "Atom", "Atom.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], "Atom", CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_ShouldPrioritizeBreadthFirst_InDownwardSearch() + { + var searchRoot = P("SearchRoot"); + + _fs.AddFile(P("SearchRoot", "Level1", "Level2", "Target.csproj"), new("")); + _fs.AddFile(P("SearchRoot", "Level1_Sibling", "Target.csproj"), new("")); + _fs.AddDirectory(searchRoot); + + _fs.Directory.SetCurrentDirectory(searchRoot); + + var result = await RunCommand.Handle([], "Target", CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_WithDotCsprojSubject_FindsProject_ReturnsZero() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "MyBuild.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], "MyBuild.csproj", CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_WithDotCsprojSubject_ProjectNotFound_ReturnsOne() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], "Missing.csproj", CancellationToken.None); + + result.ShouldBe(1); + } + + [Test] + public async Task Handle_WithDotCsSubject_FindsCsFile_ReturnsZero() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "build.cs"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], "build.cs", CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_WithDotCsSubject_FileNotFound_ReturnsOne() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], "missing.cs", CancellationToken.None); + + result.ShouldBe(1); + } + + [Test] + public async Task Handle_WithEitherSubject_FindsProject_ReturnsZero() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "mybuild.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], "mybuild", CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_WithEitherSubject_FindsCsFile_ReturnsZero() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "mybuild.cs"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], "mybuild", CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_WithEitherSubject_NotFound_ReturnsOne() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], "mybuild", CancellationToken.None); + + result.ShouldBe(1); + } + + [Test] + public async Task Handle_WithEitherSubject_PrefersProjectOverCsFile() + { + // Both exist — project wins (project is checked first) + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "mybuild.csproj"), new("")); + _fs.AddFile(P("work", "mybuild.cs"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + // Should succeed regardless of which one it picks + var result = await RunCommand.Handle([], "mybuild", CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_WithEmptySubject_FindsAtomProject_ReturnsZero() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "_atom.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], string.Empty, CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_WithEmptySubject_FindsBuildProject_ReturnsZero() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "_build.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], string.Empty, CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_WithEmptySubject_NothingFound_ReturnsOne() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], string.Empty, CancellationToken.None); + + result.ShouldBe(1); + } + + [Test] + public async Task Handle_SubjectWithNewlines_IsSanitizedBeforeSearch() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "mybuild.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + // Newlines in subject should be stripped + var result = await RunCommand.Handle([], "my\nbuild", CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_SubjectWithCarriageReturn_IsSanitizedBeforeSearch() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "mybuild.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], "my\rbuild", CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_SubjectWithSurroundingQuotes_IsTrimmed() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "mybuild.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], "\"mybuild\"", CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_SubjectWithSingleQuotes_IsTrimmed() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "mybuild.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], "'mybuild'", CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_SubjectWithSurroundingSpaces_IsTrimmed() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "mybuild.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], " mybuild ", CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_WithRunArgs_DoesNotAffectSearchOrReturnCode() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "_atom.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + // Extra run args should not affect discovery or mock return + var result = await RunCommand.Handle(["--target", "Build", "--param", "Foo=Bar"], + string.Empty, + CancellationToken.None); + + result.ShouldBe(0); + } + + [Test] + public async Task Handle_CacheMiss_PerformsRestore_AndWritesCache() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "_atom.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], string.Empty, CancellationToken.None); + + result.ShouldBe(0); + RunCommand.LastUsedNoRestore.ShouldBeFalse(); + + _fs + .File + .Exists(P("work", "obj", ".atom-restore.hash")) + .ShouldBeTrue(); + } + + [Test] + public async Task Handle_CacheHit_SkipsRestore() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "_atom.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + // First run performs a restore and writes the cache. + await RunCommand.Handle([], string.Empty, CancellationToken.None); + + // Simulate the restore output that 'dotnet restore' would produce. + _fs.AddFile(P("work", "obj", "project.assets.json"), new("")); + + // Second run with unchanged inputs should skip the restore. + var result = await RunCommand.Handle([], string.Empty, CancellationToken.None); + + result.ShouldBe(0); + RunCommand.LastUsedNoRestore.ShouldBeTrue(); + } + + [Test] + public async Task Handle_CacheInvalidated_WhenInputChanges_PerformsRestore() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "_atom.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + await RunCommand.Handle([], string.Empty, CancellationToken.None); + _fs.AddFile(P("work", "obj", "project.assets.json"), new("")); + + // Change the project file content - the cached hash should no longer match. + _fs.File.WriteAllText(P("work", "_atom.csproj"), ""); + + var result = await RunCommand.Handle([], string.Empty, CancellationToken.None); + + result.ShouldBe(0); + RunCommand.LastUsedNoRestore.ShouldBeFalse(); + } + + [Test] + public async Task Handle_CacheInvalidated_WhenDirectoryPackagesChanges_PerformsRestore() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "_atom.csproj"), new("")); + _fs.AddFile(P("work", "Directory.Packages.props"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + await RunCommand.Handle([], string.Empty, CancellationToken.None); + _fs.AddFile(P("work", "obj", "project.assets.json"), new("")); + + // A walked-up restore input changed - restore should run again. + _fs.File.WriteAllText(P("work", "Directory.Packages.props"), ""); + + var result = await RunCommand.Handle([], string.Empty, CancellationToken.None); + + result.ShouldBe(0); + RunCommand.LastUsedNoRestore.ShouldBeFalse(); + } + + [Test] + public async Task Handle_NoRestoreCacheFlag_AlwaysPerformsRestore() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "_atom.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + await RunCommand.Handle([], string.Empty, CancellationToken.None); + _fs.AddFile(P("work", "obj", "project.assets.json"), new("")); + + // Even though the cache would normally hit, the opt-out forces a restore. + var result = await RunCommand.Handle([], string.Empty, true, CancellationToken.None); + + result.ShouldBe(0); + RunCommand.LastUsedNoRestore.ShouldBeFalse(); + } + + [Test] + public async Task Handle_NoRestoreCacheEnvVar_AlwaysPerformsRestore() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "_atom.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + await RunCommand.Handle([], string.Empty, CancellationToken.None); + _fs.AddFile(P("work", "obj", "project.assets.json"), new("")); + + Environment.SetEnvironmentVariable("ATOM_NO_RESTORE_CACHE", "1"); + + var result = await RunCommand.Handle([], string.Empty, CancellationToken.None); + + result.ShouldBe(0); + RunCommand.LastUsedNoRestore.ShouldBeFalse(); + } + + [Test] + public async Task Handle_BuildCacheMiss_PerformsBuild_AndWritesCache() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "_atom.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + var result = await RunCommand.Handle([], string.Empty, CancellationToken.None); + + result.ShouldBe(0); + RunCommand.LastUsedNoBuild.ShouldBeFalse(); + + _fs + .File + .Exists(P("work", "obj", ".atom-build.hash")) + .ShouldBeTrue(); + } + + [Test] + public async Task Handle_BuildCacheHit_SkipsBuild() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "_atom.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + // First run builds and writes the cache. + await RunCommand.Handle([], string.Empty, CancellationToken.None); + + // Simulate the build output that 'dotnet build' would produce. + _fs.AddFile(P("work", "bin", "Debug", "net10.0", "_atom.dll"), new("")); + + // Second run with unchanged inputs should skip the build entirely. + var result = await RunCommand.Handle([], string.Empty, CancellationToken.None); + + result.ShouldBe(0); + RunCommand.LastUsedNoBuild.ShouldBeTrue(); + RunCommand.LastUsedNoRestore.ShouldBeFalse(); // --no-build supersedes --no-restore + } + + [Test] + public async Task Handle_BuildCacheInvalidated_WhenSourceChanges_PerformsBuild() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "_atom.csproj"), new("")); + _fs.AddFile(P("work", "Program.cs"), new("// v1")); + _fs.Directory.SetCurrentDirectory(workDir); + + await RunCommand.Handle([], string.Empty, CancellationToken.None); + _fs.AddFile(P("work", "bin", "Debug", "net10.0", "_atom.dll"), new("")); + + // A source file changed - the build can no longer be skipped. + _fs.File.WriteAllText(P("work", "Program.cs"), "// v2"); + + var result = await RunCommand.Handle([], string.Empty, CancellationToken.None); + + result.ShouldBe(0); + RunCommand.LastUsedNoBuild.ShouldBeFalse(); + } + + [Test] + public async Task Handle_NoRestoreCacheFlag_AlsoForcesBuild() + { + var workDir = P("work"); + _fs.AddDirectory(workDir); + _fs.AddFile(P("work", "_atom.csproj"), new("")); + _fs.Directory.SetCurrentDirectory(workDir); + + await RunCommand.Handle([], string.Empty, CancellationToken.None); + _fs.AddFile(P("work", "bin", "Debug", "net10.0", "_atom.dll"), new("")); + + // Even though the build cache would hit, the opt-out forces a full build. + var result = await RunCommand.Handle([], string.Empty, true, CancellationToken.None); + + result.ShouldBe(0); + RunCommand.LastUsedNoBuild.ShouldBeFalse(); + } +} diff --git a/tests/Invex.Atom.Tool.Tests/_usings.cs b/tests/Invex.Atom.Tool.Tests/_usings.cs new file mode 100644 index 00000000..cf5a0ebf --- /dev/null +++ b/tests/Invex.Atom.Tool.Tests/_usings.cs @@ -0,0 +1,7 @@ +global using System.IO.Abstractions; +global using ConsoleAppFramework; +global using System.IO.Abstractions.TestingHelpers; +global using System.Runtime.InteropServices; +global using Invex.Atom.Tool.Commands; +global using NUnit.Framework; +global using Shouldly; diff --git a/tests/Invex.Atom.Workflows.Tests/Builds/DependentTargetsBuild.cs b/tests/Invex.Atom.Workflows.Tests/Builds/DependentTargetsBuild.cs new file mode 100644 index 00000000..a052bf55 --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/Builds/DependentTargetsBuild.cs @@ -0,0 +1,28 @@ +namespace Invex.Atom.Workflows.Tests.Builds; + +[BuildDefinition] +public partial class DependentTargetsBuild : WorkflowBuildDefinition, IFirstTarget, ISecondTarget +{ + public override IReadOnlyList Workflows => + [ + new("dependent-workflow") + { + Triggers = [new TestWorkflowTrigger()], + Targets = [new(nameof(IFirstTarget.FirstTarget)), new(nameof(ISecondTarget.SecondTarget))], + Types = [new TestWorkflowType()], + }, + ]; +} + +public interface IFirstTarget +{ + Target FirstTarget => t => t.Executes(() => Task.CompletedTask); +} + +public interface ISecondTarget +{ + Target SecondTarget => + t => t + .DependsOn(nameof(IFirstTarget.FirstTarget)) + .Executes(() => Task.CompletedTask); +} diff --git a/tests/Invex.Atom.Workflows.Tests/Builds/EmptyTargetsWorkflowBuild.cs b/tests/Invex.Atom.Workflows.Tests/Builds/EmptyTargetsWorkflowBuild.cs new file mode 100644 index 00000000..9ea40728 --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/Builds/EmptyTargetsWorkflowBuild.cs @@ -0,0 +1,15 @@ +namespace Invex.Atom.Workflows.Tests.Builds; + +[BuildDefinition] +public partial class EmptyTargetsWorkflowBuild : WorkflowBuildDefinition, IWorkflowBuildDefinition +{ + public override IReadOnlyList Workflows => + [ + new("empty-targets-workflow") + { + Triggers = [new TestWorkflowTrigger()], + Targets = [], + Types = [new TestWorkflowType()], + }, + ]; +} diff --git a/tests/Invex.Atom.Workflows.Tests/Builds/EmptyWorkflowBuild.cs b/tests/Invex.Atom.Workflows.Tests/Builds/EmptyWorkflowBuild.cs new file mode 100644 index 00000000..e1a28512 --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/Builds/EmptyWorkflowBuild.cs @@ -0,0 +1,8 @@ +namespace Invex.Atom.Workflows.Tests.Builds; + +[BuildDefinition] +public partial class EmptyWorkflowBuild : WorkflowBuildDefinition +{ + // No workflows + public override IReadOnlyList Workflows => []; +} diff --git a/tests/Invex.Atom.Workflows.Tests/Builds/IInterfaceBuild.cs b/tests/Invex.Atom.Workflows.Tests/Builds/IInterfaceBuild.cs new file mode 100644 index 00000000..d178189e --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/Builds/IInterfaceBuild.cs @@ -0,0 +1,20 @@ +namespace Invex.Atom.Workflows.Tests.Builds; + +[BuildDefinition] +public interface IInterfaceBuild : IWorkflowBuildDefinition, IInterfaceTarget +{ + IReadOnlyList IWorkflowBuildDefinition.Workflows => + [ + new("single-workflow") + { + Triggers = [new TestWorkflowTrigger()], + Targets = [new(nameof(InterfaceTarget))], + Types = [new TestWorkflowType()], + }, + ]; +} + +public interface IInterfaceTarget +{ + Target InterfaceTarget => t => t.Executes(() => Task.CompletedTask); +} diff --git a/tests/Invex.Atom.Workflows.Tests/Builds/MatrixWorkflowBuild.cs b/tests/Invex.Atom.Workflows.Tests/Builds/MatrixWorkflowBuild.cs new file mode 100644 index 00000000..ab22851f --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/Builds/MatrixWorkflowBuild.cs @@ -0,0 +1,22 @@ +namespace Invex.Atom.Workflows.Tests.Builds; + +[BuildDefinition] +public partial class MatrixWorkflowBuild : WorkflowBuildDefinition, IWorkflowBuildDefinition, ISingleTarget +{ + public override IReadOnlyList Workflows => + [ + new("matrix-workflow") + { + Triggers = [new TestWorkflowTrigger()], + Targets = + [ + new WorkflowTargetDefinition(nameof(ISingleTarget.SingleTarget)).WithMatrixDimensions( + new MatrixDimension("os") + { + Values = ["ubuntu-latest", "windows-latest"], + }), + ], + Types = [new TestWorkflowType()], + }, + ]; +} diff --git a/tests/Invex.Atom.Workflows.Tests/Builds/MultiTypeBuild.cs b/tests/Invex.Atom.Workflows.Tests/Builds/MultiTypeBuild.cs new file mode 100644 index 00000000..2b73c346 --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/Builds/MultiTypeBuild.cs @@ -0,0 +1,15 @@ +namespace Invex.Atom.Workflows.Tests.Builds; + +[BuildDefinition] +public partial class MultiTypeBuild : WorkflowBuildDefinition, IWorkflowBuildDefinition, ISingleTarget +{ + public override IReadOnlyList Workflows => + [ + new("multi-type-workflow") + { + Triggers = [new TestWorkflowTrigger()], + Targets = [new(nameof(ISingleTarget.SingleTarget))], + Types = [new TestWorkflowType(), new TestWorkflowType()], + }, + ]; +} diff --git a/tests/Invex.Atom.Workflows.Tests/Builds/SingleTargetBuild.cs b/tests/Invex.Atom.Workflows.Tests/Builds/SingleTargetBuild.cs new file mode 100644 index 00000000..b02bb276 --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/Builds/SingleTargetBuild.cs @@ -0,0 +1,20 @@ +namespace Invex.Atom.Workflows.Tests.Builds; + +[BuildDefinition] +public partial class SingleTargetBuild : WorkflowBuildDefinition, IWorkflowBuildDefinition, ISingleTarget +{ + public override IReadOnlyList Workflows => + [ + new("single-workflow") + { + Triggers = [new TestWorkflowTrigger()], + Targets = [new(nameof(ISingleTarget.SingleTarget))], + Types = [new TestWorkflowType()], + }, + ]; +} + +public interface ISingleTarget +{ + Target SingleTarget => t => t.Executes(() => Task.CompletedTask); +} diff --git a/tests/Invex.Atom.Workflows.Tests/Builds/WorkflowWithOptionsBuild.cs b/tests/Invex.Atom.Workflows.Tests/Builds/WorkflowWithOptionsBuild.cs new file mode 100644 index 00000000..7261fa5c --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/Builds/WorkflowWithOptionsBuild.cs @@ -0,0 +1,20 @@ +namespace Invex.Atom.Workflows.Tests.Builds; + +[BuildDefinition] +public partial class WorkflowWithOptionsBuild : WorkflowBuildDefinition, IWorkflowBuildDefinition, ISingleTarget +{ + public override IReadOnlyList Workflows => + [ + new("options-workflow") + { + Triggers = [new TestWorkflowTrigger()], + Options = [new TestWorkflowOption("workflow-level")], + Targets = + [ + new WorkflowTargetDefinition(nameof(ISingleTarget.SingleTarget)).WithOptions( + new TestWorkflowOption("target-level")), + ], + Types = [new TestWorkflowType()], + }, + ]; +} diff --git a/tests/Invex.Atom.Workflows.Tests/Invex.Atom.Workflows.Tests.csproj b/tests/Invex.Atom.Workflows.Tests/Invex.Atom.Workflows.Tests.csproj new file mode 100644 index 00000000..e7ad7f6c --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/Invex.Atom.Workflows.Tests.csproj @@ -0,0 +1,59 @@ + + + + net10.0;net9.0;net8.0 + false + invex.atom.tests + + + + + + + + + + + + + + + + + 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/DecSm.Atom.TestUtils/TestWorkflowTrigger.cs b/tests/Invex.Atom.Workflows.Tests/TestUtils/TestWorkflowTrigger.cs similarity index 59% rename from DecSm.Atom.TestUtils/TestWorkflowTrigger.cs rename to tests/Invex.Atom.Workflows.Tests/TestUtils/TestWorkflowTrigger.cs index bde2172f..6470b6c2 100644 --- a/DecSm.Atom.TestUtils/TestWorkflowTrigger.cs +++ b/tests/Invex.Atom.Workflows.Tests/TestUtils/TestWorkflowTrigger.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.TestUtils; +namespace Invex.Atom.Workflows.Tests.TestUtils; [PublicAPI] public sealed record TestWorkflowTrigger : IWorkflowTrigger; diff --git a/DecSm.Atom.TestUtils/TestWorkflowType.cs b/tests/Invex.Atom.Workflows.Tests/TestUtils/TestWorkflowType.cs similarity index 70% rename from DecSm.Atom.TestUtils/TestWorkflowType.cs rename to tests/Invex.Atom.Workflows.Tests/TestUtils/TestWorkflowType.cs index 2ef27c79..672a3025 100644 --- a/DecSm.Atom.TestUtils/TestWorkflowType.cs +++ b/tests/Invex.Atom.Workflows.Tests/TestUtils/TestWorkflowType.cs @@ -1,4 +1,4 @@ -namespace DecSm.Atom.TestUtils; +namespace Invex.Atom.Workflows.Tests.TestUtils; [PublicAPI] public sealed record TestWorkflowType : IWorkflowType diff --git a/tests/Invex.Atom.Workflows.Tests/TestUtils/TestWorkflowWriter.cs b/tests/Invex.Atom.Workflows.Tests/TestUtils/TestWorkflowWriter.cs new file mode 100644 index 00000000..a9a222bf --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/TestUtils/TestWorkflowWriter.cs @@ -0,0 +1,24 @@ +namespace Invex.Atom.Workflows.Tests.TestUtils; + +[PublicAPI] +internal sealed class TestWorkflowWriter : IWorkflowWriter +{ + public bool IsOutdated { get; init; } + + public bool ThrowOnWrite { get; init; } + + public List GeneratedWorkflows { get; } = []; + + public Task Generate(WorkflowModel workflow, CancellationToken cancellationToken = default) + { + if (ThrowOnWrite) + throw new StepFailedException("TestWorkflowWriter is configured to throw on write."); + + GeneratedWorkflows.Add(workflow); + + return Task.CompletedTask; + } + + public Task CheckForOutdatedWorkflow(WorkflowModel workflow, CancellationToken cancellationToken = default) => + Task.FromResult(IsOutdated); +} diff --git a/tests/Invex.Atom.Workflows.Tests/TestUtils/WorkflowTestUtils.cs b/tests/Invex.Atom.Workflows.Tests/TestUtils/WorkflowTestUtils.cs new file mode 100644 index 00000000..aaed813e --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/TestUtils/WorkflowTestUtils.cs @@ -0,0 +1,29 @@ +namespace Invex.Atom.Workflows.Tests.TestUtils; + +[PublicAPI] +internal static class WorkflowTestUtils +{ + public static IHost CreateWorkflowTestHost( + TestConsole? console = null, + MockFileSystem? fileSystem = null, + CommandLineArgs? commandLineArgs = null, + TestBuildIdProvider? buildIdProvider = null, + TestBuildVersionProvider? buildVersionProvider = null, + TestWorkflowWriter? workflowWriter = null, + Action? configure = null) + where T : BuildDefinition => + Atom.TestUtils.TestUtils.CreateTestHost(console, + fileSystem, + commandLineArgs ?? new CommandLineArgs(true, [new CommandArg(nameof(IGen.Gen))]), + buildIdProvider, + buildVersionProvider, + x => + { + if (workflowWriter != null) + x.Services.AddSingleton(workflowWriter); + else + x.Services.AddSingleton(); + + configure?.Invoke(x); + }); +} diff --git a/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.DependentTargetsBuild_WorkflowWithDependentTargets_CreatesWorkflow.verified.txt b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.DependentTargetsBuild_WorkflowWithDependentTargets_CreatesWorkflow.verified.txt new file mode 100644 index 00000000..bfe0b09f --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.DependentTargetsBuild_WorkflowWithDependentTargets_CreatesWorkflow.verified.txt @@ -0,0 +1,29 @@ +{ + IsOutdated: false, + ThrowOnWrite: false, + GeneratedWorkflows: [ + { + Name: dependent-workflow, + Triggers: [ + {} + ], + Jobs: [ + { + Name: FirstTarget, + TargetStep: { + Name: FirstTarget + } + }, + { + Name: SecondTarget, + TargetStep: { + Name: SecondTarget + }, + JobDependencies: [ + FirstTarget + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.EmptyTargetsWorkflowBuild_WorkflowWithEmptyTargets_CreatesWorkflow.verified.txt b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.EmptyTargetsWorkflowBuild_WorkflowWithEmptyTargets_CreatesWorkflow.verified.txt new file mode 100644 index 00000000..e6dad03c --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.EmptyTargetsWorkflowBuild_WorkflowWithEmptyTargets_CreatesWorkflow.verified.txt @@ -0,0 +1,12 @@ +{ + IsOutdated: false, + ThrowOnWrite: false, + GeneratedWorkflows: [ + { + Name: empty-targets-workflow, + Triggers: [ + {} + ] + } + ] +} \ No newline at end of file diff --git a/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.EmptyWorkflowBuild_NoWorkflows_DoesNotCreateWorkflow.verified.txt b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.EmptyWorkflowBuild_NoWorkflows_DoesNotCreateWorkflow.verified.txt new file mode 100644 index 00000000..c2e5247b --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.EmptyWorkflowBuild_NoWorkflows_DoesNotCreateWorkflow.verified.txt @@ -0,0 +1,4 @@ +{ + IsOutdated: false, + ThrowOnWrite: false +} \ No newline at end of file diff --git a/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.InterfaceBuild_CreatesWorkflow.verified.txt b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.InterfaceBuild_CreatesWorkflow.verified.txt new file mode 100644 index 00000000..6c9576d0 --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.InterfaceBuild_CreatesWorkflow.verified.txt @@ -0,0 +1,20 @@ +{ + IsOutdated: false, + ThrowOnWrite: false, + GeneratedWorkflows: [ + { + Name: single-workflow, + Triggers: [ + {} + ], + Jobs: [ + { + Name: InterfaceTarget, + TargetStep: { + Name: InterfaceTarget + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.MatrixWorkflowBuild_WorkflowWithMatrix_CreatesWorkflow.verified.txt b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.MatrixWorkflowBuild_WorkflowWithMatrix_CreatesWorkflow.verified.txt new file mode 100644 index 00000000..0f1f8bc9 --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.MatrixWorkflowBuild_WorkflowWithMatrix_CreatesWorkflow.verified.txt @@ -0,0 +1,33 @@ +{ + IsOutdated: false, + ThrowOnWrite: false, + GeneratedWorkflows: [ + { + Name: matrix-workflow, + Triggers: [ + {} + ], + Jobs: [ + { + Name: SingleTarget, + TargetStep: { + Name: SingleTarget, + MatrixDimensions: [ + { + Name: os, + Values: [ + { + Value: ubuntu-latest + }, + { + Value: windows-latest + } + ] + } + ] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.Workflow_Generates_ForEach_Type.verified.txt b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.Workflow_Generates_ForEach_Type.verified.txt new file mode 100644 index 00000000..0da5ba03 --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.Workflow_Generates_ForEach_Type.verified.txt @@ -0,0 +1,34 @@ +{ + IsOutdated: false, + ThrowOnWrite: false, + GeneratedWorkflows: [ + { + Name: multi-type-workflow, + Triggers: [ + {} + ], + Jobs: [ + { + Name: SingleTarget, + TargetStep: { + Name: SingleTarget + } + } + ] + }, + { + Name: multi-type-workflow, + Triggers: [ + {} + ], + Jobs: [ + { + Name: SingleTarget, + TargetStep: { + Name: SingleTarget + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.Workflow_WithOptions_Generates_WithOptions.verified.txt b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.Workflow_WithOptions_Generates_WithOptions.verified.txt new file mode 100644 index 00000000..fcb61c3f --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.Workflow_WithOptions_Generates_WithOptions.verified.txt @@ -0,0 +1,33 @@ +{ + IsOutdated: false, + ThrowOnWrite: false, + GeneratedWorkflows: [ + { + Name: options-workflow, + Triggers: [ + {} + ], + Options: [ + { + Value: workflow-level + } + ], + Jobs: [ + { + Name: SingleTarget, + TargetStep: { + Name: SingleTarget, + Options: [ + { + Value: workflow-level + }, + { + Value: target-level + } + ] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.cs b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.cs new file mode 100644 index 00000000..23f3b25a --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/WorkflowBuildTests.cs @@ -0,0 +1,263 @@ +namespace Invex.Atom.Workflows.Tests; + +[TestFixture] +internal sealed class WorkflowBuildTests +{ + [Test] + public async Task EmptyWorkflowBuild_NoWorkflows_DoesNotCreateWorkflow() + { + // Arrange + var writer = new TestWorkflowWriter(); + using var host = CreateWorkflowTestHost(workflowWriter: writer); + + // Act + await host.RunAsync(); + + // Assert + writer.GeneratedWorkflows.ShouldBeEmpty(); + await Verify(writer); + } + + [Test] + public async Task DependentTargetsBuild_WorkflowWithDependentTargets_CreatesWorkflow() + { + // Arrange + var writer = new TestWorkflowWriter(); + using var host = CreateWorkflowTestHost(workflowWriter: writer); + + // Act + await host.RunAsync(); + + // Assert + writer + .GeneratedWorkflows + .ShouldHaveSingleItem() + .ShouldSatisfyAllConditions(workflow => + { + workflow.Name.ShouldBe("dependent-workflow"); + + workflow + .Triggers + .ShouldHaveSingleItem() + .ShouldBeOfType(); + + workflow.Jobs.Count.ShouldBe(2); + + workflow + .Jobs[0] + .ShouldSatisfyAllConditions(job => job.Name.ShouldBe("FirstTarget")); + + workflow + .Jobs[1] + .ShouldSatisfyAllConditions(job => + { + job.Name.ShouldBe("SecondTarget"); + + job + .JobDependencies + .ShouldHaveSingleItem() + .ShouldBe("FirstTarget"); + }); + }); + + await Verify(writer); + } + + [Test] + public async Task EmptyTargetsWorkflowBuild_WorkflowWithEmptyTargets_CreatesWorkflow() + { + // Arrange + var writer = new TestWorkflowWriter(); + using var host = CreateWorkflowTestHost(workflowWriter: writer); + + // Act + await host.RunAsync(); + + // Assert + writer + .GeneratedWorkflows + .ShouldHaveSingleItem() + .ShouldSatisfyAllConditions(workflow => + { + workflow.Name.ShouldBe("empty-targets-workflow"); + + workflow + .Triggers + .ShouldHaveSingleItem() + .ShouldBeOfType(); + + workflow.Jobs.ShouldBeEmpty(); + }); + + await Verify(writer); + } + + [Test] + public async Task WriterError_SetsExitCode() + { + // Arrange + var writer = new TestWorkflowWriter + { + ThrowOnWrite = true, + }; + + using var host = CreateWorkflowTestHost(workflowWriter: writer); + + // Act + await host.RunAsync(); + + // Assert + Environment.ExitCode.ShouldBe(1); + + Environment.ExitCode = 0; // Reset exit code for other tests + } + + [Test] + public async Task MatrixWorkflowBuild_WorkflowWithMatrix_CreatesWorkflow() + { + // Arrange + var writer = new TestWorkflowWriter(); + using var host = CreateWorkflowTestHost(workflowWriter: writer); + + // Act + await host.RunAsync(); + + // Assert + writer + .GeneratedWorkflows + .ShouldHaveSingleItem() + .ShouldSatisfyAllConditions(workflow => + { + workflow.Name.ShouldBe("matrix-workflow"); + + workflow + .Triggers + .ShouldHaveSingleItem() + .ShouldBeOfType(); + + workflow.Jobs.Count.ShouldBe(1); + + workflow + .Jobs[0] + .ShouldSatisfyAllConditions(job => + { + job.Name.ShouldBe("SingleTarget"); + + job.TargetStep.ShouldSatisfyAllConditions(step => + { + step.Name.ShouldBe("SingleTarget"); + + step + .MatrixDimensions + .ShouldHaveSingleItem() + .ShouldSatisfyAllConditions(dimension => + { + dimension.Name.ShouldBe("os"); + dimension.Values.Count.ShouldBe(2); + + dimension + .Values[0] + .ShouldBe("ubuntu-latest"); + + dimension + .Values[1] + .ShouldBe("windows-latest"); + }); + }); + }); + }); + + await Verify(writer); + } + + [Test] + public async Task Workflow_Generates_ForEach_Type() + { + // Arrange + var writer = new TestWorkflowWriter(); + using var host = CreateWorkflowTestHost(workflowWriter: writer); + + // Act + await host.RunAsync(); + + // Assert + writer.GeneratedWorkflows.Count.ShouldBe(2); + + await Verify(writer); + } + + [Test] + public async Task Workflow_WithOptions_Generates_WithOptions() + { + // Arrange + var writer = new TestWorkflowWriter(); + using var host = CreateWorkflowTestHost(workflowWriter: writer); + + // Act + await host.RunAsync(); + + // Assert + writer + .GeneratedWorkflows + .ShouldHaveSingleItem() + .ShouldSatisfyAllConditions(workflow => + { + workflow.Name.ShouldBe("options-workflow"); + + workflow + .Triggers + .ShouldHaveSingleItem() + .ShouldBeOfType(); + + workflow.Options.Count.ShouldBe(1); + + workflow + .Options[0] + .ShouldBeOfType() + .ShouldSatisfyAllConditions(option => + { + option.ShouldBeOfType(); + option.Value.ShouldBe("workflow-level"); + }); + }); + + await Verify(writer); + } + + [Test] + public async Task InterfaceBuild_CreatesWorkflow() + { + // Arrange + var writer = new TestWorkflowWriter(); + using var host = CreateWorkflowTestHost(workflowWriter: writer); + + // Act + await host.RunAsync(); + + // Assert + writer + .GeneratedWorkflows + .ShouldHaveSingleItem() + .ShouldSatisfyAllConditions(workflow => + { + workflow.Name.ShouldBe("single-workflow"); + + workflow + .Triggers + .ShouldHaveSingleItem() + .ShouldBeOfType(); + + workflow.Jobs.Count.ShouldBe(1); + + workflow + .Jobs[0] + .ShouldSatisfyAllConditions(job => + { + job.Name.ShouldBe("InterfaceTarget"); + job.TargetStep.Name.ShouldBe("InterfaceTarget"); + }); + }); + + await Verify(writer); + } +} diff --git a/tests/Invex.Atom.Workflows.Tests/WorkflowTargetDefinitionTests.cs b/tests/Invex.Atom.Workflows.Tests/WorkflowTargetDefinitionTests.cs new file mode 100644 index 00000000..39583e73 --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/WorkflowTargetDefinitionTests.cs @@ -0,0 +1,147 @@ +namespace Invex.Atom.Workflows.Tests; + +[TestFixture] +internal sealed class WorkflowTargetDefinitionTests +{ + [Test] + public void CreateModel_WithNoOptions_ReturnsModelWithWorkflowOptions() + { + var target = new WorkflowTargetDefinition("MyTarget"); + IBuildOption[] workflowOptions = [new TestWorkflowOption("wf")]; + + var model = target.CreateModel(workflowOptions); + + model.Name.ShouldBe("MyTarget"); + model.MatrixDimensions.ShouldBeEmpty(); + + model + .Options + .OfType() + .ShouldContain(o => o.Value == "wf"); + } + + [Test] + public void CreateModel_WithTargetOptions_MergesWorkflowAndTargetOptions() + { + var target = new WorkflowTargetDefinition("MyTarget") + { + Options = [new TestWorkflowOption("target")], + }; + + var workflowOptions = new IBuildOption[] { new TestWorkflowOption("workflow") }; + var model = target.CreateModel(workflowOptions); + + model.Options.Count.ShouldBe(2); + + model + .Options + .OfType() + .ShouldContain(o => o.Value == "workflow"); + + model + .Options + .OfType() + .ShouldContain(o => o.Value == "target"); + } + + [Test] + public void CreateModel_PreservesMatrixDimensions() + { + var dim = new MatrixDimension("os") + { + Values = ["linux", "windows"], + }; + + var target = new WorkflowTargetDefinition("MyTarget") + { + MatrixDimensions = [dim], + }; + + var model = target.CreateModel([]); + + model.MatrixDimensions.ShouldHaveSingleItem(); + + model + .MatrixDimensions[0] + .Name + .ShouldBe("os"); + } + + [Test] + public void WithMatrixDimensions_AddsDimensions() + { + var dim1 = new MatrixDimension("os") + { + Values = ["ubuntu-latest"], + }; + + var dim2 = new MatrixDimension("dotnet") + { + Values = ["8.0", "9.0"], + }; + + var target = new WorkflowTargetDefinition("T").WithMatrixDimensions(dim1, dim2); + + target.MatrixDimensions.Count.ShouldBe(2); + target.MatrixDimensions.ShouldContain(d => d.Name == "os"); + target.MatrixDimensions.ShouldContain(d => d.Name == "dotnet"); + } + + [Test] + public void WithMatrixDimensions_PreservesExistingDimensions() + { + var existing = new MatrixDimension("existing"); + + var target = new WorkflowTargetDefinition("T") + { + MatrixDimensions = [existing], + }; + + var newDim = new MatrixDimension("new"); + + var updated = target.WithMatrixDimensions(newDim); + + updated.MatrixDimensions.Count.ShouldBe(2); + updated.MatrixDimensions.ShouldContain(d => d.Name == "existing"); + updated.MatrixDimensions.ShouldContain(d => d.Name == "new"); + } + + [Test] + public void WithMatrixDimensions_ReturnsNewInstance() + { + var target = new WorkflowTargetDefinition("T"); + var updated = target.WithMatrixDimensions(new MatrixDimension("os")); + updated.ShouldNotBeSameAs(target); + } + + [Test] + public void WithOptions_AddsOptions() + { + var target = new WorkflowTargetDefinition("T"); + var withOpts = target.WithOptions(new TestWorkflowOption("val")); + + withOpts.Options.ShouldHaveSingleItem(); + ((TestWorkflowOption)withOpts.Options[0]).Value.ShouldBe("val"); + } + + [Test] + public void WithOptions_PreservesExistingOptions() + { + var target = new WorkflowTargetDefinition("T") + { + Options = [new TestWorkflowOption("existing")], + }; + + var updated = target.WithOptions(new TestWorkflowOption("new")); + + updated.Options.Count.ShouldBe(2); + } + + [Test] + public void WithOptions_ReturnsNewInstance() + { + var target = new WorkflowTargetDefinition("T"); + var updated = target.WithOptions(new TestWorkflowOption("x")); + updated.ShouldNotBeSameAs(target); + } +} diff --git a/tests/Invex.Atom.Workflows.Tests/WorkflowTriggersTests.cs b/tests/Invex.Atom.Workflows.Tests/WorkflowTriggersTests.cs new file mode 100644 index 00000000..0407fe19 --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/WorkflowTriggersTests.cs @@ -0,0 +1,127 @@ +namespace Invex.Atom.Workflows.Tests; + +[TestFixture] +internal sealed class WorkflowTriggersTests +{ + [Test] + public void Manual_ReturnsSingletonManualTrigger() + { + var t1 = WorkflowTriggers.Manual; + var t2 = WorkflowTriggers.Manual; + t1.ShouldBeSameAs(t2); + t1.ShouldBeOfType(); + t1.Inputs.ShouldBeNull(); + } + + [Test] + public void ManualWithInputs_ReturnsManualTriggerWithInputs() + { + var input = new ManualStringInput("MyInput", "desc", false); + var trigger = WorkflowTriggers.ManualWithInputs(input); + trigger.Inputs.ShouldNotBeNull(); + trigger.Inputs!.ShouldHaveSingleItem(); + } + + [Test] + public void PullIntoMain_ReturnsSingletonWithMainBranch() + { + var t = WorkflowTriggers.PullIntoMain; + t.ShouldBeOfType(); + t.IncludedBranches.ShouldContain("main"); + } + + [Test] + public void PushToMain_ReturnsSingletonWithMainBranch() + { + var t = WorkflowTriggers.PushToMain; + t.ShouldBeOfType(); + t.IncludedBranches.ShouldContain("main"); + } + + [Test] + public void PullInto_ReturnsTriggerWithSpecifiedBranches() + { + var t = WorkflowTriggers.PullInto("main", "develop"); + t.IncludedBranches.ShouldBe(["main", "develop"]); + } + + [Test] + public void PushTo_ReturnsTriggerWithSpecifiedBranches() + { + var t = WorkflowTriggers.PushTo("release"); + t.IncludedBranches.ShouldBe(["release"]); + } + + [Test] + public void PullRequest_SetsAllProperties() + { + var t = WorkflowTriggers.PullRequest(["main"], ["dev"], ["src/**"], ["docs/**"], ["opened"]); + + t.IncludedBranches.ShouldContain("main"); + t.ExcludedBranches.ShouldContain("dev"); + t.IncludedPaths.ShouldContain("src/**"); + t.ExcludedPaths.ShouldContain("docs/**"); + t.Types.ShouldContain("opened"); + } + + [Test] + public void Push_SetsAllProperties() + { + var t = WorkflowTriggers.Push(["main"], ["dev"], ["src/**"], ["docs/**"], ["v*"], ["v0*"]); + + t.IncludedBranches.ShouldContain("main"); + t.ExcludedBranches.ShouldContain("dev"); + t.IncludedPaths.ShouldContain("src/**"); + t.ExcludedPaths.ShouldContain("docs/**"); + t.IncludedTags.ShouldContain("v*"); + t.ExcludedTags.ShouldContain("v0*"); + } + + [Test] + public void GitPushTrigger_Defaults_AreEmpty() + { + var t = new GitPushTrigger(); + t.IncludedBranches.ShouldBeEmpty(); + t.ExcludedBranches.ShouldBeEmpty(); + t.IncludedPaths.ShouldBeEmpty(); + t.ExcludedPaths.ShouldBeEmpty(); + t.IncludedTags.ShouldBeEmpty(); + t.ExcludedTags.ShouldBeEmpty(); + } + + [Test] + public void GitPullRequestTrigger_Defaults_AreEmpty() + { + var t = new GitPullRequestTrigger(); + t.IncludedBranches.ShouldBeEmpty(); + t.ExcludedBranches.ShouldBeEmpty(); + t.IncludedPaths.ShouldBeEmpty(); + t.ExcludedPaths.ShouldBeEmpty(); + t.Types.ShouldBeEmpty(); + } + + [Test] + public void ManualStringInput_InitializesCorrectly() + { + var i = new ManualStringInput("my-input", "My description", true); + i.Name.ShouldBe("my-input"); + i.Description.ShouldBe("My description"); + i.Required.ShouldBe(true); + } + + [Test] + public void ManualBoolInput_InitializesCorrectly() + { + var i = new ManualBoolInput("flag", "A flag", false); + i.Name.ShouldBe("flag"); + i.Required.ShouldBe(false); + } + + [Test] + public void ManualChoiceInput_InitializesCorrectly() + { + var i = new ManualChoiceInput("env", "Environment", null, ["dev", "staging", "prod"]); + i.Name.ShouldBe("env"); + i.Choices.ShouldBe(["dev", "staging", "prod"]); + } +} diff --git a/tests/Invex.Atom.Workflows.Tests/_usings.cs b/tests/Invex.Atom.Workflows.Tests/_usings.cs new file mode 100644 index 00000000..ecb87fc5 --- /dev/null +++ b/tests/Invex.Atom.Workflows.Tests/_usings.cs @@ -0,0 +1,22 @@ +global using System.IO.Abstractions.TestingHelpers; +global using Invex.Atom.Build.Args; +global using Invex.Atom.Build.BuildOptions; +global using Invex.Atom.Build.Definition; +global using Invex.Atom.Build.Exceptions; +global using Invex.Atom.Build.Params; +global using Invex.Atom.TestUtils; +global using Invex.Atom.Workflows.Definition; +global using Invex.Atom.Workflows.Definition.Triggers; +global using Invex.Atom.Workflows.Model; +global using Invex.Atom.Workflows.Tests.Builds; +global using Invex.Atom.Workflows.Tests.TestUtils; +global using Invex.Atom.Workflows.Writer; +global using JetBrains.Annotations; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using NUnit.Framework; +global using Shouldly; +global using Spectre.Console.Testing; +global using static Invex.Atom.Workflows.Tests.TestUtils.WorkflowTestUtils; +global using Target = Invex.Atom.Build.Definition.Target; diff --git a/toc.yml b/toc.yml new file mode 100644 index 00000000..22f32209 --- /dev/null +++ b/toc.yml @@ -0,0 +1,6 @@ +- name: Guides + href: docs/toc.yml + homepage: docs/getting-started/introduction.md +- name: API Reference + href: api/ +