diff --git a/.github/actions/push-nuget-packages/action.yml b/.github/actions/push-nuget-packages/action.yml index 4c6889c..c1c1428 100644 --- a/.github/actions/push-nuget-packages/action.yml +++ b/.github/actions/push-nuget-packages/action.yml @@ -29,6 +29,10 @@ inputs: required: false default: '' description: API key used to authenticate the push (required when publish-mode is api-key) + dry-run: + required: false + default: 'false' + description: If true, skips real release downloads, NuGet trusted-publishing login, the actual push to NuGet and logs what would happen instead. runs: using: composite steps: @@ -68,9 +72,17 @@ runs: shell: pwsh run: | $version = "${{ inputs.version }}" - gh release download $version --repo "${{ github.repository }}" --pattern "*.nupkg" --dir nugets - if ($LASTEXITCODE -ne 0) { - throw "Failed to download release assets for tag '$version'" + + if ('${{ inputs.dry-run }}' -eq 'true') + { + Write-Output "[dry-run] Would download assets from published release '$version'." + } + else + { + gh release download $version --repo "${{ github.repository }}" --pattern "*.nupkg" --dir nugets + if ($LASTEXITCODE -ne 0) { + throw "Failed to download release assets for tag '$version'" + } } env: GH_TOKEN: ${{ github.token }} @@ -79,15 +91,23 @@ runs: shell: pwsh run: | $name = "${{ inputs.version }}" - $releases = gh api "repos/${{ github.repository }}/releases" | ConvertFrom-Json - $release = $releases | Where-Object { $_.name -eq $name } | Select-Object -First 1 - if (-not $release) { - throw "Could not find a draft release named '$name'" + + if ('${{ inputs.dry-run }}' -eq 'true') + { + Write-Output "[dry-run] Would look up and download assets from draft release named '$name'." } - Write-Output "Found draft release id $($release.id) with tag '$($release.tag_name)' for '$name'" - gh release download $release.tag_name --repo "${{ github.repository }}" --pattern "*.nupkg" --dir nugets - if ($LASTEXITCODE -ne 0) { - throw "Failed to download release assets for tag '$($release.tag_name)'" + else + { + $releases = gh api "repos/${{ github.repository }}/releases" | ConvertFrom-Json + $release = $releases | Where-Object { $_.name -eq $name } | Select-Object -First 1 + if (-not $release) { + throw "Could not find a draft release named '$name'" + } + Write-Output "Found draft release id $($release.id) with tag '$($release.tag_name)' for '$name'" + gh release download $release.tag_name --repo "${{ github.repository }}" --pattern "*.nupkg" --dir nugets + if ($LASTEXITCODE -ne 0) { + throw "Failed to download release assets for tag '$($release.tag_name)'" + } } env: GH_TOKEN: ${{ github.token }} @@ -99,7 +119,7 @@ runs: $domain = ([Uri]"${{ inputs.feed-url }}").GetLeftPart([System.UriPartial]::Authority) Write-Output "domain=$domain" >> $env:GITHUB_OUTPUT - name: Get NuGet trusted publishing API key - if: inputs.publish-mode == 'trusted-publishing' + if: inputs.publish-mode == 'trusted-publishing' && inputs.dry-run != 'true' uses: NuGet/login@v1 id: login with: @@ -109,20 +129,28 @@ runs: - name: Push NuGet packages shell: pwsh run: | - $getChildItemParams = @{ Path = 'nugets/*'; Include = '*.nupkg' } - $excludePattern = "${{ inputs.exclude-package-pattern }}" - if ($excludePattern) { $getChildItemParams.Exclude = $excludePattern } - $packages = Get-ChildItem @getChildItemParams $source = "${{ inputs.feed-url }}" - $apiKey = if ("${{ inputs.publish-mode }}" -eq 'trusted-publishing') { "${{ steps.login.outputs.NUGET_API_KEY }}" } else { "${{ inputs.api-key }}" } - Write-Output "Pushing $($packages.Count) NuGet package(s) to $source`:" - $packages | ForEach-Object { Write-Output " - $($_.Name)" } - $failed = @() - foreach ($package in $packages) + + if ('${{ inputs.dry-run }}' -eq 'true') { - dotnet nuget push $package.FullName --source $source --api-key $apiKey --skip-duplicate - if ($LASTEXITCODE -ne 0) { $failed += $package.Name } + Write-Output "[dry-run] Would push NuGet package(s) found under 'nugets/' to $source." } - if ($failed.Count -gt 0) { - throw "Failed to push $($failed.Count) package(s): $($failed -join ', ')" + else + { + $getChildItemParams = @{ Path = 'nugets/*'; Include = '*.nupkg' } + $excludePattern = "${{ inputs.exclude-package-pattern }}" + if ($excludePattern) { $getChildItemParams.Exclude = $excludePattern } + $packages = Get-ChildItem @getChildItemParams + $apiKey = if ("${{ inputs.publish-mode }}" -eq 'trusted-publishing') { "${{ steps.login.outputs.NUGET_API_KEY }}" } else { "${{ inputs.api-key }}" } + Write-Output "Pushing $($packages.Count) NuGet package(s) to $source`:" + $packages | ForEach-Object { Write-Output " - $($_.Name)" } + $failed = @() + foreach ($package in $packages) + { + dotnet nuget push $package.FullName --source $source --api-key $apiKey --skip-duplicate + if ($LASTEXITCODE -ne 0) { $failed += $package.Name } + } + if ($failed.Count -gt 0) { + throw "Failed to push $($failed.Count) package(s): $($failed -join ', ')" + } } diff --git a/.github/workflows/CI-actions.yml b/.github/workflows/CI-actions.yml index 0546aa1..9696ec6 100644 --- a/.github/workflows/CI-actions.yml +++ b/.github/workflows/CI-actions.yml @@ -7,19 +7,13 @@ on: paths: - '.github/actions/**' jobs: - test-actions: - runs-on: ubuntu-latest - defaults: - run: - shell: pwsh - steps: - - name: Placeholder - run: echo "Placeholder for testing actions" + test-push-nuget-packages: + uses: ./.github/workflows/test-push-nuget-packages.yml validate-action-tests: # So we can add new tests without having to update branch protection rules name: Validate action test results - needs: [test-actions] + needs: [test-push-nuget-packages] if: always() runs-on: ubuntu-latest defaults: diff --git a/.github/workflows/test-push-nuget-packages.yml b/.github/workflows/test-push-nuget-packages.yml new file mode 100644 index 0000000..5a314ff --- /dev/null +++ b/.github/workflows/test-push-nuget-packages.yml @@ -0,0 +1,120 @@ +name: Test - push-nuget-packages +on: + workflow_call: +jobs: + test-push-nuget-packages: + name: ${{ matrix.scenario }} + runs-on: ubuntu-latest + defaults: + run: + shell: pwsh + strategy: + fail-fast: false + matrix: + include: + - scenario: invalid publish-mode + publish-mode: not-a-mode + release-state: published + nuget-user: '' + api-key: '' + feed-url: https://nuget.example.org + expect-failure: true + - scenario: invalid release-state + publish-mode: api-key + release-state: not-a-state + nuget-user: '' + api-key: some-key + feed-url: https://nuget.example.org + expect-failure: true + - scenario: trusted-publishing missing nuget-user + publish-mode: trusted-publishing + release-state: published + nuget-user: '' + api-key: '' + feed-url: https://nuget.example.org + expect-failure: true + - scenario: trusted-publishing with conflicting api-key + publish-mode: trusted-publishing + release-state: published + nuget-user: someuser + api-key: some-key + feed-url: https://nuget.example.org + expect-failure: true + - scenario: api-key mode missing api-key + publish-mode: api-key + release-state: published + nuget-user: '' + api-key: '' + feed-url: https://nuget.example.org + expect-failure: true + - scenario: invalid feed-url + publish-mode: api-key + release-state: published + nuget-user: '' + api-key: some-key + feed-url: not-a-url + expect-failure: true + - scenario: valid (published, trusted-publishing) + publish-mode: trusted-publishing + release-state: published + nuget-user: test-user + api-key: '' + feed-url: https://nuget.example.org + expect-failure: false + - scenario: valid (published, api-key) + publish-mode: api-key + release-state: published + nuget-user: '' + api-key: test-key + feed-url: https://nuget.example.org + expect-failure: false + - scenario: valid (draft, trusted-publishing) + publish-mode: trusted-publishing + release-state: draft + nuget-user: test-user + api-key: '' + feed-url: https://nuget.example.org + expect-failure: false + - scenario: valid (draft, api-key) + publish-mode: api-key + release-state: draft + nuget-user: '' + api-key: test-key + feed-url: https://nuget.example.org + expect-failure: false + steps: + - name: Checkout + uses: actions/checkout@v7.0.1 + - name: Push NuGet packages + id: push + continue-on-error: ${{ matrix.expect-failure }} + uses: ./.github/actions/push-nuget-packages + with: + version: 1.2.3 + release-state: ${{ matrix.release-state }} + publish-mode: ${{ matrix.publish-mode }} + nuget-user: ${{ matrix.nuget-user }} + api-key: ${{ matrix.api-key }} + feed-url: ${{ matrix.feed-url }} + dry-run: true + - name: Verify + run: | + $expectFailure = '${{ matrix.expect-failure }}' -eq 'true' + $outcome = '${{ steps.push.outcome }}' + + if ($expectFailure) + { + if ($outcome -ne 'failure') + { + throw "Expected push-nuget-packages to fail for scenario '${{ matrix.scenario }}', but outcome was '$outcome'." + } + Write-Output "OK: failed as expected ($outcome)" + } + else + { + if ($outcome -ne 'success') + { + throw "Expected push-nuget-packages to succeed for scenario '${{ matrix.scenario }}', but outcome was '$outcome'." + } + Write-Output "OK: succeeded as expected ($outcome)" + }