diff --git a/.github/actions/push-nuget-packages/action.yml b/.github/actions/push-nuget-packages/action.yml index 9d09da9..4c6889c 100644 --- a/.github/actions/push-nuget-packages/action.yml +++ b/.github/actions/push-nuget-packages/action.yml @@ -1,49 +1,128 @@ name: Push NuGet Packages -description: Downloads .nupkg release assets and publishes them to a NuGet feed using OIDC trusted publishing +description: Downloads .nupkg release assets and publishes them to a NuGet feed using OIDC trusted publishing or a static API key inputs: version: required: true - description: Release tag to download package assets from + description: > + Release to download package assets from. + release-state: + required: true + description: Whether version identifies a 'published' release (by tag) or a 'draft' release (by name). One of 'published' or 'draft' exclude-package-pattern: required: false default: '' description: Glob pattern of package filenames to exclude from the push - nuget-user: + publish-mode: required: true - description: NuGet trusted publishing user - nuget-source-domain: + description: How to authenticate the push. One of 'trusted-publishing' or 'api-key' + nuget-user: + required: false + default: '' + description: NuGet trusted publishing user (required when publish-mode is trusted-publishing) + feed-url: required: true - description: NuGet feed domain used for both the trusted-publishing token service and the package source + description: > + NuGet package source URL, used as the push source. For publish-mode + 'trusted-publishing' its domain also extracted to build the token + service URL and audience. + api-key: + required: false + default: '' + description: API key used to authenticate the push (required when publish-mode is api-key) runs: using: composite steps: + - name: Validate inputs + shell: pwsh + run: | + $publishMode = "${{ inputs.publish-mode }}" + $validModes = @('trusted-publishing', 'api-key') + if ($validModes -notcontains $publishMode) { + throw "publish-mode must be one of: $($validModes -join ', '). Got '$publishMode'." + } + $releaseState = "${{ inputs.release-state }}" + $validReleaseStates = @('published', 'draft') + if ($validReleaseStates -notcontains $releaseState) { + throw "release-state must be one of: $($validReleaseStates -join ', '). Got '$releaseState'." + } + if ($publishMode -eq 'trusted-publishing' -and -not "${{ inputs.nuget-user }}") { + throw "nuget-user is required when publish-mode is 'trusted-publishing'." + } + if ($publishMode -eq 'trusted-publishing' -and "${{ inputs.api-key }}") { + throw "api-key must not be set when publish-mode is 'trusted-publishing'." + } + if ($publishMode -eq 'api-key' -and -not "${{ inputs.api-key }}") { + throw "api-key is required when publish-mode is 'api-key'." + } + $feedUrl = "${{ inputs.feed-url }}" + $parsedUri = $null + if (-not [Uri]::TryCreate($feedUrl, [UriKind]::Absolute, [ref]$parsedUri)) { + throw "feed-url must be a valid absolute URL. Got '$feedUrl'." + } - name: Setup .NET SDK uses: actions/setup-dotnet@v6 with: dotnet-version: 'latest' - - name: Download assets from release + - name: Download assets from published release + if: inputs.release-state == 'published' + 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'" + } + env: + GH_TOKEN: ${{ github.token }} + - name: Download assets from draft release + if: inputs.release-state == 'draft' shell: pwsh run: | - gh release download "${{ inputs.version }}" --repo "${{ github.repository }}" --pattern "*.nupkg" --dir nugets + $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'" + } + 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 }} + - name: Resolve NuGet feed domain + id: resolve + if: inputs.publish-mode == 'trusted-publishing' + shell: pwsh + run: | + $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' uses: NuGet/login@v1 id: login with: user: ${{ inputs.nuget-user }} - token-service-url: ${{ inputs.nuget-source-domain }}/api/v2/token - audience: ${{ inputs.nuget-source-domain }} - - name: Push NuGet packages to production feed + token-service-url: ${{ steps.resolve.outputs.domain }}/api/v2/token + audience: ${{ steps.resolve.outputs.domain }} + - 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 - Write-Output "Pushing $($packages.Count) NuGet package(s) to ${{ inputs.nuget-source-domain }}:" + $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) { - dotnet nuget push $package.FullName --source ${{ inputs.nuget-source-domain }}/api/v2/package --api-key ${{ steps.login.outputs.NUGET_API_KEY }} + 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/push-nuget-packages-staging.yml b/.github/workflows/push-nuget-packages-staging.yml deleted file mode 100644 index 5833a58..0000000 --- a/.github/workflows/push-nuget-packages-staging.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Push NuGet Packages to Staging -on: - workflow_call: - inputs: - version: - required: true - type: string - exclude-package-pattern: - required: false - type: string - default: '' -jobs: - push-nuget-packages: - permissions: - id-token: write - contents: write - runs-on: ubuntu-latest - defaults: - run: - shell: pwsh - steps: - - name: Setup .NET SDK - uses: actions/setup-dotnet@v6 - with: - dotnet-version: 'latest' - - name: Download assets from draft release - 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 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 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Push NuGet packages to staging feed - run: | - $getChildItemParams = @{ Path = 'nugets/*'; Include = '*.nupkg' } - $excludePattern = "${{ inputs.exclude-package-pattern }}" - if ($excludePattern) { $getChildItemParams.Exclude = $excludePattern } - $packages = Get-ChildItem @getChildItemParams - Write-Output "Pushing $($packages.Count) NuGet package(s) to ${{ vars.PARTICULAR_TESTING_FEED_URL }}:" - $packages | ForEach-Object { Write-Output " - $($_.Name)" } - foreach ($package in $packages) - { - dotnet nuget push $package.FullName --source ${{ vars.PARTICULAR_TESTING_FEED_URL }} --api-key ${{ secrets.FEEDZIO_PUBLISH_API_KEY }} - }