ci: publish to NuGet via Trusted Publishing, not a long-lived key - #63
Merged
Conversation
Both publish paths in this org relied on `secrets.NUGET_API_KEY`, and several
workflows commented that the secret was absent so the push was a clean no-op.
That was checked with `gh secret list`, which reports REPOSITORY secrets only.
Measured against the API:
repos/{owner}/{repo}/actions/organization-secrets -> 1: NUGET_API_KEY (2026-02-28)
repos/{owner}/{repo}/actions/secrets -> 0
actions/variables + organization-variables -> 0
So the key resolved and the push was live, not inert.
Switches to Trusted Publishing (OIDC): the login step exchanges the GitHub OIDC
token for a key valid ~1 h, so no long-lived secret is stored. NuGet/login is
pinned by digest and kept adjacent to the push so the key cannot expire between.
The job-level `permissions:` block restates what the workflow-level one granted:
a job-level block REPLACES it rather than adding to it, so omitting `contents`
would have broken the GitHub Release steps.
The login step is DELIBERATELY UNGUARDED. This job only runs on a release
trigger, so publishing is expected: a missing policy must fail loudly rather
than skip and leave a tagged version unpublished behind a green check. Pattern
taken from RoselineMCP#114, whose argument is better than a silent no-op here.
BEFORE MERGING, in this order:
1. create the Trusted Publishing policy on nuget.org, under the account that
OWNS the package id, naming this repository and this exact workflow file;
2. set the NUGET_USER secret (the nuget.org profile name, not a credential);
3. cut one real release and confirm the package appears on the flatcontainer
index;
4. only then delete NUGET_API_KEY.
Deleting the key first leaves no way back if the policy is wrong.
Full procedure: repo-audit/TRUSTED_PUBLISHING.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
This repo's publish step guarded on
secrets.NUGET_API_KEY. Several workflows in this org carry acomment saying that secret is absent, so the push is "a clean no-op". That was checked with
gh secret list, which reports repository secrets only.Measured against the API instead:
actions/organization-secretsNUGET_API_KEY, created 2026-02-28actions/secrets(repo)actions/variables·organization-variablesThe key resolved. The push was live, not inert.
The change
Trusted Publishing (OIDC): the login step exchanges the GitHub OIDC token for a key valid ~1 h, so
no long-lived secret is stored anywhere.
NuGet/loginis pinned by digest and kept adjacent to thepush so the key cannot expire in between.
Two details that are easy to get wrong:
permissions:block restates what the workflow-level one granted. A job-levelblock replaces the workflow-level one rather than adding to it — omitting
contentswould havebroken the GitHub Release steps.
publishing is expected: a missing policy must fail loudly rather than skip and leave a tagged
version unpublished behind a green check. Pattern taken from
RoselineMCP#114, whose argumentis better than a silent no-op here.
package id, naming this repository and this exact workflow file.
NUGET_USERsecret — the nuget.org profile name, not a credential.NUGET_API_KEY.Deleting the key first leaves no way back if the policy is wrong. Merging before step 1 means the
next tag fails — loudly, by design, but it fails.
Full procedure and the portfolio-wide worklist:
repo-audit/TRUSTED_PUBLISHING.md.