Visitar URL original
feat(publish): Pass NUGET_USER to Craft for nuget.org trusted publishing by sentry-junior[bot] · Pull Request #10366 · getsentry/publish · GitHub
Skip to content

feat(publish): Pass NUGET_USER to Craft for nuget.org trusted publishing - #10366

Closed
sentry-junior[bot] wants to merge 1 commit into
mainfrom
feat/nuget-trusted-publishing
Closed

sentry-junior[bot] wants to merge 1 commit into
mainfrom
feat/nuget-trusted-publishing

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Companion to getsentry/craft#972. With this PR, NuGet releases can use nuget.org trusted publishing (GitHub OIDC) instead of the long-lived NUGET_API_TOKEN. That token expiring is what failed #10356.

Refs #6754

Change

The Craft step now gets NUGET_USER: ${{ vars.NUGET_USER }}. id-token: write was already granted at the workflow level (for mcp-registry), so nothing else is needed.

  • Safe to merge before setup: if the variable is unset, NUGET_USER is empty. Craft then uses NUGET_API_TOKEN as it does today.
  • Rollback: delete the variable.

Steps that need people

  1. nuget.org: a member of the sentry nuget.org owner adds a Trusted Publishing policy with owner getsentry, repository publish, workflow file publish.yml and environment production. The policy owner is sentry, which covers all of its packages; a Sentry* glob is optional. Prefer a bot or shared account, because the policy stops working if the person who created it leaves the org.
  2. Craft: merge and release feat(nuget): Support nuget.org trusted publishing with GitHub OIDC craft#972. This workflow runs getsentry/craft:latest.
  3. This repo's settings → Actions variables: set NUGET_USER to the nuget.org profile name of the policy creator from step 1. Use the profile name, not the email.
  4. Release sentry-dotnet and check that the Craft log shows Requesting a temporary NuGet API key.
  5. After a release succeeds: delete the NUGET_API_TOKEN secret and revoke the key on nuget.org.

#skip-changelog

via Ricardo Oliveira.

--

View Session [Sentry]

Craft uses nuget.org trusted publishing (GitHub OIDC) instead of
NUGET_API_TOKEN when NUGET_USER is set. Reading it from a repository
variable keeps this a no-op until the variable is set, and makes rolling
back a matter of deleting it.

Refs #6754

Co-Authored-By: Ricardo Oliveira <ricardo.oliveira@sentry.io>
@BYK

BYK commented Nov 11, 2026

Copy link
Copy Markdown
Member

Actually, I think we should go back to Craft and also allow setting this via the config, wdyt?

@sentry-junior

sentry-junior Bot commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Agreed, the config is a better place for it. Pushed to getsentry/craft#972 (d9b6db3): the nuget target now takes trustedPublishingUser in .craft.yml. NUGET_USER still works as a fallback, and the config value wins when both are set. Workspace expansion copies the option to every package target.

Why this is better: each repo opts in through its own .craft.yml, so publish.yml doesn't switch every NuGet release over at once. id-token: write is already granted there, so nothing in this repo has to change.

That makes this PR unnecessary. I'm leaving it open for you to close, in case you'd still like the env var path. The sentry-dotnet side becomes trustedPublishingUser: <policy creator> on its nuget target, plus a minVersion bump once Craft is released.

@ric-oliv
ric-oliv marked this pull request as ready for review November 11, 2026 15:39
@ric-oliv
ric-oliv requested a review from a team as a code owner November 11, 2026 15:39
@ric-oliv

Copy link
Copy Markdown
Member

Closing in favor of the trustedPublishingUser config option in getsentry/craft#972. Thanks!

@ric-oliv ric-oliv closed this Nov 11, 2026
@ric-oliv
ric-oliv deleted the feat/nuget-trusted-publishing branch November 11, 2026 15:46
ric-oliv added a commit to getsentry/craft that referenced this pull request Nov 11, 2026
)

Adds [nuget.org trusted
publishing](https://learn.microsoft.com/nuget/nuget-org/trusted-publishing)
to the `nuget` target. Publishing no longer needs a long-lived
`NUGET_API_TOKEN`, so there is no key to rotate. The sentry-dotnet
6.13.0 release (getsentry/publish#10356) failed because that key
expired.

Refs getsentry/publish#6754

## What changes

- **`trustedPublishingUser` turns it on.** Set it on the `nuget` target
in `.craft.yml`, or set the `NUGET_USER` env var; the config value wins.
When it is set, the target requests a GitHub Actions OIDC token with
audience `https://www.nuget.org`, exchanges it at
`https://www.nuget.org/api/v2/token` for a temporary API key, and pushes
with that key. This is the same exchange
[`NuGet/login`](https://github.com/NuGet/login) does. Without it, the
target uses `NUGET_API_TOKEN` exactly as before. Workspace expansion
copies the option to every package target.
- **The key is fetched when the target first pushes, not when it is
created.** Craft creates every target before publishing anything.
sentry-dotnet expands into 24 `nuget` targets that run one after
another, so a key fetched at startup could expire partway through. Each
target gets its own key, its packages share it, and Craft fetches a new
one once it is 45 minutes old. nuget.org keys last 1 hour.
- **Errors show up early:**
- With trusted publishing enabled but no OIDC available (the job lacks
`id-token: write`), the target fails when it is created instead of
falling back to the token without telling anyone.
- Trusted publishing with a custom `serverUrl` fails, so a nuget.org key
is never sent to another feed.
- When nuget.org rejects the exchange, the error includes its response
and a reminder that the user must be the person who created the policy,
not the org.
- **The key stays out of logs.** It is passed to `dotnet nuget push` as
the `${NUGET_API_KEY}` placeholder. `spawnProcess` fills it in after
logging, as it already did for `${NUGET_API_TOKEN}`. On GitHub Actions
the temporary key is also masked with `::add-mask::`.
- The GitHub OIDC token request moves from `mcpRegistry.ts` to
`src/utils/githubOidc.ts`, so both targets use the same code. The
mcp-registry target behaves the same as before.
- Docs: `targets/nuget.md` gets the new option and a Trusted Publishing
section.

## Rollout (needs people, not code)

1. **nuget.org:** a member of the `sentry` nuget.org owner (ideally a
bot or shared account, not a person who might leave) adds a Trusted
Publishing policy:
- Repository owner `getsentry`, repository `publish`, workflow file
`publish.yml`, environment `production`
- Owner: `sentry`. The policy covers every package that owner owns. You
can narrow it with a `Sentry*` package glob.
2. **This PR:** merge and release Craft. getsentry/publish runs
`getsentry/craft:latest`, so no pin needs bumping, and `publish.yml`
already grants `id-token: write`.
3. **getsentry/sentry-dotnet:** add `trustedPublishingUser: <nuget.org
profile name of the policy creator>` to the `nuget` target in
`.craft.yml`, and bump `minVersion` to the Craft release from step 2.
4. Publish a sentry-dotnet release and check the log for `Requesting a
temporary NuGet API key`.
5. Once a release has succeeded: delete the `NUGET_API_TOKEN` secret
from getsentry/publish and revoke the key on nuget.org.

With the config option, getsentry/publish#10366 (passing `NUGET_USER`
from a repository variable) is no longer needed.

Rollback: remove `trustedPublishingUser` from `.craft.yml`. Craft goes
back to `NUGET_API_TOKEN` as long as that secret is still there.

## Testing

- New tests in `nuget.test.ts` cover:
  - token mode, including the placeholder
  - the config errors
- enabling it from the target config (which wins over `NUGET_USER`), and
workspace expansion carrying the option
  - the OIDC audience and the exchange request
  - one key shared by the packages of a target
  - getting a new key after 45 minutes
  - masking
  - a rejected exchange
- `mcpRegistry.test.ts` passes unchanged.
- `pnpm lint`, `tsc --noEmit` and `pnpm build` pass.
- Nothing has been tested against nuget.org yet. That needs the policy
from step 1.
- In the Junior sandbox, 24 `prepare-dry-run` e2e tests and one git test
fail because of the sandbox's git identity settings. They fail the same
way on `master`.

<!-- junior-request-attribution:start -->
via **Ricardo Oliveira**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:slack%3AC0CACQ70E8Z%3A1794364886.114019 -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/slack%3AC0CACQ70E8Z%3A1794364886.114019)
[[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3AC0CACQ70E8Z%3A1794364886.114019/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: Ricardo Oliveira <ricardo.oliveira@sentry.io>
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.

2 participants