Publish to NuGet via trusted publishing, fix release permissions - #153
Merged
Merged
Conversation
…sions Exchanges the GitHub OIDC token for a nuget.org API key that lives one hour, so no long-lived key is stored. The expired NUGET_API_KEY is what blocked 3.0.0; this removes the class of failure rather than restarting its clock. Done inline rather than with NuGet/login, which is not on the org allow-list and would fail at startup. One OIDC token mints exactly one key, so the exchange sits immediately before the push. Two fixes Didier found in review: - The job inherited contents: read from the workflow, but its last step runs gh release create, which needs contents: write. Neither 3.0.0 attempt reached it — both died at the push — so it would have surfaced as a package published to NuGet with no GitHub release. The deleted release.yml carried write-all, which is why 2.6.0's release exists. - gh release create had no existence guard, so a re-run after a partial failure errored with "release already exists". --skip-duplicate is dropped too: it made a re-tag of an already-published version report success while publishing nothing.
Trusted publishing matches on the workflow filename and environment, so the exchange can only be tested from deploy.yml itself — a separate workflow would fail to match even when everything is correct. workflow_dispatch runs the exchange and stops before packing or publishing, and takes an optional nuget_user input so the profile name can be tried without editing the workflow. Tag-only steps are gated on github.ref_type.
workflow_dispatch can be run against a tag, where ref_type is 'tag' but the event is not a push. Pack and the GitHub release were gated on ref_type while the push was gated on the event, so a dry run against a tag would have created a release. Tags are the only push trigger, so one rule covers both.
Records that publishing is keyless and that the nuget.org policy matches on repository, workflow filename and environment — renaming any of them breaks publishing silently. Adds the manual Deploy run as a way to check the credential path without cutting a release, which is the gap that let an expired key sit unnoticed from March until a release needed it.
bsneed
approved these changes
Sep 30, 2026
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.
Addresses the 403 that blocked 3.0.0 and two issues from @didiergarcia's review of #150.
Trusted publishing instead of a stored key
The 3.0.0 push failed with
403 (The specified API key is invalid, has expired, or does not have permission…).NUGET_API_KEYwas last rotated 2025-03-04; nuget.org keys cap at 365 days. Rotating it restarts the same clock, so this moves to trusted publishing — the GitHub OIDC token is exchanged for a key that lives one hour.Done inline rather than with
NuGet/login@v1, which is not on the segmentio allow-list and would fail at workflow startup. Same approach asanalytics-ruby, where the inline RubyGems exchange has now published successfully. The endpoint and payload are the ones Microsoft documents for the GitLab path:The blocker nobody reached
The publish job inherited
contents: readfrom the workflow, but its final step runsgh release create, which needscontents: write. Both 3.0.0 attempts died at the push first, so it never surfaced — the moment the credential worked, the package would have published and then the release step would 403, leaving NuGet published with no GitHub release. The deletedrelease.ymlcarriedpermissions: write-all, which is why 2.6.0's release exists; removing it took the write permission with it.The job now declares
id-token: writeandcontents: writeexplicitly.Smaller fixes
gh release createis guarded bygh release view, so a re-run after a partial failure no longer errors with "release already exists".--skip-duplicatedropped. It made re-tagging an already-published version report success while publishing nothing — which nearly happened here.Before this can release
Two things outside the repo:
segmentio, RepositoryAnalytics-CSharp, Workflow Filedeploy.yml(filename only), Environmentdeployment. It can be owned by the Segment org, which already owns the package.NUGET_USERsecret in thedeploymentenvironment — the nuget.org profile name, not an email address.The step fails with a clear message if
NUGET_USERis missing rather than producing an opaque exchange error.Re-tagging is required
deploy.ymlruns from the tagged commit, so3.0.0has to be deleted and re-pushed after this merges. A re-run of the existing run would replay the old workflow file. (Rotating a secret would survive a re-run; a workflow change does not.)Not addressed here
From the same review, left for separate PRs: deleting
build.yml, pinning or deletingpublish-e2e-cli.yml, restoringE2E_TESTS_TOKENfore2e-tests.yml, andTestVersioninTests/AnalyticsTest.csbeing a tautology (_analytics.VersionreturnsVersion.SegmentVersion) that cannot detect the drift #150 exists to prevent.