Skip to content

ci(release): publish via NuGet trusted publishing (OIDC), drop the stored API key - #15

Merged
moisesja merged 1 commit into
mainfrom
ci/nuget-trusted-publishing
Jun 26, 2026
Merged

ci(release): publish via NuGet trusted publishing (OIDC), drop the stored API key#15
moisesja merged 1 commit into
mainfrom
ci/nuget-trusted-publishing

Conversation

@moisesja

Copy link
Copy Markdown
Owner

nuget.org now promotes trusted publishing over long-lived API keys. The first v1.0.0 publish failed because no NUGET_API_KEY secret was set; rather than store a long-lived key, this switches release.yml to OIDC trusted publishing — no stored secret at all.

Pipeline change (release.yml)

  • permissions: adds id-token: write (lets the job mint a GitHub OIDC token).
  • New NuGet/login@v1 step (verified against the action's action.yml/README) exchanges the OIDC token for a short-lived (~1h) nuget.org API key, output NUGET_API_KEY.
  • The push uses ${{ steps.nuget-login.outputs.NUGET_API_KEY }}; --skip-duplicate + the symbol-package glob are unchanged.
  • user: is read from a repo variable NUGET_USER (the nuget.org account username, not an email).

One-time maintainer setup (required before re-tagging)

  1. nuget.org → your avatar → Trusted Publishing → create a policy:
    • Repository Owner: moisesja
    • Repository: credentials-dotnet
    • Workflow File: release.yml (filename only — no .github/workflows/ path)
    • Environment: nuget-release
      The policy is owner-scoped (no package-name field), so it covers the brand-new Credentials.Core / .Extensions.DependencyInjection / .Rdfc IDs on first publish. (A new policy on a private repo starts in a 7-day temporary-active window; this repo is public, and a successful publish makes it permanent.)
  2. GitHub → Settings → Secrets and variables → Actions → Variables → add repository variable NUGET_USER = your nuget.org username.

After merge + setup

The v1.0.0 tag currently points at the pre-fix commit (old release.yml), and its publish failed — nothing was published. So once this merges and the setup above is done, the v1.0.0 tag is moved to the merge commit and re-pushed to trigger a clean OIDC publish. (Safe to move since 1.0.0 was never published.)

🤖 Generated with Claude Code

…ored API key

nuget.org now promotes trusted publishing over long-lived API keys. Switch
release.yml to OIDC:
- Add the id-token:write permission so the job can mint a GitHub OIDC token.
- Add a NuGet/login@v1 step that exchanges the OIDC token for a short-lived
  (~1h) nuget.org API key (no NUGET_API_KEY secret is stored anywhere).
- Push with that ephemeral key; keep --skip-duplicate and the symbol-package glob.

Requires (one-time, maintainer): a Trusted Publishing policy on nuget.org for
owner=moisesja, repo=credentials-dotnet, workflow file=release.yml,
environment=nuget-release; and a repository variable NUGET_USER = the nuget.org
account username. The policy is owner-scoped (no package-name field), so it
covers the brand-new Credentials.* package IDs on first publish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@moisesja moisesja left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good motivation — dropping a long-lived secret in favour of OIDC is the right call, and the change is small and focused. Two issues worth fixing before merge.


1. Unpinned action SHA — high severity

uses: NuGet/login@v1

The v1 tag is mutable; anyone who can push to the NuGet/login repository can silently redirect it. This step runs with id-token: write, meaning a compromised version of the action could steal your OIDC token and exchange it for a short-lived nuget.org key before you notice.

Pin to the exact commit SHA instead:

uses: NuGet/login@<commit-sha>  # v1.x.x

Check the current tip SHA at https://github.com/NuGet/login/commits/v1 before merging and annotate it with the tag name for human readability.

The same concern applies to the pre-existing actions/checkout@v4, actions/setup-dotnet@v4, and actions/upload-artifact@v4, though those do not hold an OIDC write grant. The NuGet login step is the urgent one; the others are worth a follow-up.


2. Step output used inline — confirm masking

--api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}"

Step outputs are not automatically masked by GitHub Actions the way secrets.* values are. If NuGet/login@v1 does not call ::add-mask:: internally, the key appears in plain text in the run log.

Safe pattern regardless of what the action does:

- name: Push to nuget.org
  env:
    NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }}
  run: |
    dotnet nuget push "./artifacts/packages/*.nupkg" \
      --api-key "$NUGET_API_KEY" \
      ...

Environment variable injection triggers GitHub's secret masker on the value at assignment time. If you've confirmed from the action's source that it calls ::add-mask::, add a comment to that effect; otherwise switch to the env: pattern.


Everything else looks correct: the environment: nuget-release gate, id-token: write scoped to the job (not workflow), --skip-duplicate, and the one-time setup instructions in the description.


Generated by Claude Code

@moisesja
moisesja merged commit a235e10 into main Jun 26, 2026
5 checks passed
@moisesja
moisesja deleted the ci/nuget-trusted-publishing branch June 26, 2026 15:13
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