Skip to content

ci: publish to NuGet.org via Trusted Publishing instead of an API key - #54

Merged
Blind-Striker merged 1 commit into
masterfrom
ci/nuget-trusted-publishing
Jul 29, 2026
Merged

ci: publish to NuGet.org via Trusted Publishing instead of an API key#54
Blind-Striker merged 1 commit into
masterfrom
ci/nuget-trusted-publishing

Conversation

@Blind-Striker

Copy link
Copy Markdown
Member

📝 Description

Replaces the long-lived NUGET_API_KEY with Trusted Publishing (GitHub OIDC → short-lived nuget.org key) for the NuGet.org publish path.

Why now. The v2.0.1 publish failed with:

Forbidden https://www.nuget.org/api/v2/package/ 233ms
error: 403 (The specified API key is invalid, has expired, or does not have
       permission to access the specified package.)

NUGET_API_KEY had aged out. The last successful publish was 2025-07-25 and nuget.org caps API keys at 365 days, so it expired almost to the day. nuget.org now also marks API keys "Not recommended" and routes key creation towards Trusted Publishing, so rotating was not the straightforward path anyway.

This is the second credential of that vintage to die silently in this repo — GIST_SECRET has been returning 401 for the badge pipeline since the same week, which is why the README test badges have served July 2025 numbers ever since. Trusted Publishing removes the whole class: nothing to rotate, store, or leak.

🔄 Type of Change

  • 🧹 Code cleanup/refactoring
  • 🐛 Bug fix
  • 💥 Breaking change

What changed

  • id-token: write added to the publish-manual job permissions.
  • A NuGet/login@v1 step runs immediately before the push — the temporary key is valid for one hour, so requesting it early would risk expiry.
  • Its output replaces the secret in the existing --package-secret argument.

The Cake nuget-push task is unchanged — it already pushes to https://api.nuget.org/v3/index.json. The GitHub Packages path still uses GITHUB_TOKEN and is untouched.

Required nuget.org policy

Already created and active:

Field Value
Package Owner LocalStack.NET
Repository Owner localstack-dotnet (#50793344)
Repository localstack-dotnet-client (#187457001)
Workflow File publish-nuget.yml
Environment (none)

Note the two distinct owners: the package owner on nuget.org is LocalStack.NET, while the repository owner is localstack-dotnet. Using the latter for both would produce a policy that does not cover these packages.

🧪 Verification

Proven end-to-end before merge, not after: LocalStack.Client.Extensions 2.0.1 was published to nuget.org by this exact workflow, run from this branch.

Pushing LocalStack.Client.Extensions.2.0.1.nupkg to 'https://www.nuget.org/api/v2/package'...
Your package was pushed.
Pushing LocalStack.Client.Extensions.2.0.1.snupkg to 'https://www.nuget.org/api/v2/symbolpackage'...
Your package was pushed.

Two things the docs left ambiguous, now settled by that run:

  • user: for an organization-owned policy — neither the Microsoft docs nor the NuGet/login README says whether this is the individual account or the org. The individual account name (localstack-dotnet) is correct.
  • Branch scope — the policy has no branch field, and the publish succeeded from this feature branch, so it binds to the workflow filename rather than a ref.

🔍 Notes

NUGET_API_KEY is left in repo secrets deliberately — it is expired and unused, but removing it is a separate cleanup once this has a few releases behind it.

The 2.0.1 publish failed with 403 "The specified API key is invalid, has
expired, or does not have permission" - NUGET_API_KEY aged out. Last successful
publish was 2025-07-25 and nuget.org caps API keys at 365 days, so it expired
almost to the day. nuget.org now also marks API keys "Not recommended" and steers
key creation towards Trusted Publishing, so rotating was not the obvious path
anyway.

Trusted Publishing exchanges a GitHub OIDC token for a short-lived nuget.org key,
which removes the whole class of failure: nothing to rotate, store or leak. This
is the second credential of that vintage to die silently in this repo - GIST_SECRET
has been returning 401 for the badge pipeline since the same week.

The job gains id-token: write, a NuGet/login@v1 step runs immediately before the
push (the temporary key is valid for one hour), and its output replaces the secret
in the existing --package-secret argument. The Cake task is unchanged, and it
already pushes to https://api.nuget.org/v3/index.json.

The GitHub Packages path still uses GITHUB_TOKEN and is untouched.

Requires the trusted publishing policy on nuget.org: package owner LocalStack.NET,
repository localstack-dotnet/localstack-dotnet-client, workflow publish-nuget.yml,
no environment. That policy is active.
@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@Blind-Striker
Blind-Striker merged commit 41e7f1c into master Jul 29, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant