From 3c369e6980f118e35ce6d5e0cfb6b42f31cb3be9 Mon Sep 17 00:00:00 2001 From: Nick Gallegos Date: Thu, 24 Sep 2026 17:09:33 -0600 Subject: [PATCH 1/4] Combine push-nuget-packages staging workflow and production action. --- .../actions/push-nuget-packages/action.yml | 79 ++++++++++++++++--- .../workflows/push-nuget-packages-staging.yml | 50 ------------ 2 files changed, 68 insertions(+), 61 deletions(-) delete mode 100644 .github/workflows/push-nuget-packages-staging.yml diff --git a/.github/actions/push-nuget-packages/action.yml b/.github/actions/push-nuget-packages/action.yml index 9d09da9..14881c0 100644 --- a/.github/actions/push-nuget-packages/action.yml +++ b/.github/actions/push-nuget-packages/action.yml @@ -1,5 +1,5 @@ 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 @@ -8,15 +8,48 @@ inputs: 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'." + } + 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: @@ -24,26 +57,50 @@ runs: - name: Download assets from release shell: pwsh run: | - gh release download "${{ inputs.version }}" --repo "${{ github.repository }}" --pattern "*.nupkg" --dir nugets + $version = "${{ inputs.version }}" + gh release download $version --repo "${{ github.repository }}" --pattern "*.nupkg" --dir nugets 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Output "Could not download release '$version' directly (likely a draft release); looking up release by name..." + $releases = gh api "repos/${{ github.repository }}/releases" | ConvertFrom-Json + $release = $releases | Where-Object { $_.name -eq $version } | Select-Object -First 1 + if (-not $release) { + throw "Could not find a release named '$version'" + } + Write-Output "Found release id $($release.id) with tag '$($release.tag_name)' for '$version'" + 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)" } 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 } 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 }} - } From c694275ed012357e0432b1dec5427d90df2a800b Mon Sep 17 00:00:00 2001 From: Nick Gallegos Date: Fri, 25 Sep 2026 15:17:41 -0600 Subject: [PATCH 2/4] Split release download logic into two separate steps with if conditions on a release-state input --- .../actions/push-nuget-packages/action.yml | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/.github/actions/push-nuget-packages/action.yml b/.github/actions/push-nuget-packages/action.yml index 14881c0..678a481 100644 --- a/.github/actions/push-nuget-packages/action.yml +++ b/.github/actions/push-nuget-packages/action.yml @@ -3,7 +3,13 @@ description: Downloads .nupkg release assets and publishes them to a NuGet feed inputs: version: required: true - description: Release tag to download package assets from + description: > + Release to download package assets from. For release-state 'published' this is + the release tag. For release-state 'draft' this is the release name (draft + releases aren't addressable by tag). + 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: '' @@ -36,6 +42,11 @@ runs: 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'." } @@ -54,23 +65,31 @@ runs: 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 2>$null + 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: | + $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) { - Write-Output "Could not download release '$version' directly (likely a draft release); looking up release by name..." - $releases = gh api "repos/${{ github.repository }}/releases" | ConvertFrom-Json - $release = $releases | Where-Object { $_.name -eq $version } | Select-Object -First 1 - if (-not $release) { - throw "Could not find a release named '$version'" - } - Write-Output "Found release id $($release.id) with tag '$($release.tag_name)' for '$version'" - 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)'" - } + throw "Failed to download release assets for tag '$($release.tag_name)'" } env: GH_TOKEN: ${{ github.token }} From 4bb81a8ad529e4652d1b2463496a3c5aad5ae3dc Mon Sep 17 00:00:00 2001 From: Nick Gallegos Date: Fri, 2 Oct 2026 10:50:54 -0600 Subject: [PATCH 3/4] Keep track of failures if there are multiple packages and don't fail if we're pushing a duplicate --- .github/actions/push-nuget-packages/action.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/actions/push-nuget-packages/action.yml b/.github/actions/push-nuget-packages/action.yml index 678a481..c967775 100644 --- a/.github/actions/push-nuget-packages/action.yml +++ b/.github/actions/push-nuget-packages/action.yml @@ -119,7 +119,12 @@ runs: $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 + 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 ', ')" } From 278cadbef088f5cb14f314d0090cb997f6cffdb2 Mon Sep 17 00:00:00 2001 From: Nick Gallegos Date: Fri, 2 Oct 2026 10:54:28 -0600 Subject: [PATCH 4/4] Update .github/actions/push-nuget-packages/action.yml --- .github/actions/push-nuget-packages/action.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/actions/push-nuget-packages/action.yml b/.github/actions/push-nuget-packages/action.yml index c967775..4c6889c 100644 --- a/.github/actions/push-nuget-packages/action.yml +++ b/.github/actions/push-nuget-packages/action.yml @@ -4,9 +4,7 @@ inputs: version: required: true description: > - Release to download package assets from. For release-state 'published' this is - the release tag. For release-state 'draft' this is the release name (draft - releases aren't addressable by tag). + 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'