feat(csharp): add NuGet trusted publishing (OIDC) support#16725
Conversation
Add config-gated Trusted Publishing publish strategy alongside the existing API-key path. Users set `api-key: OIDC` in generators.yml to opt in. Config: detect OIDC marker in getGeneratorConfig.ts, pass <USE_OIDC> through apiKeyEnvironmentVariable (mirrors npm/PyPI OIDC pattern). Workflow: when OIDC is active, emit a separate publish job with id-token: write permissions, NuGet/login@v1 for short-lived token exchange, and setup docs as comments at the top of ci.yml. Backward compatible: default behavior (api-key) is unchanged. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| run: dotnet pack <%= libraryProjectFilePath %> --no-build --no-restore -c Release | ||
|
|
||
| - name: NuGet login (OIDC → temp API key) | ||
| uses: NuGet/login@v1 |
There was a problem hiding this comment.
Unpinned third-party action
NuGet/login@v1 in workflow with id-token: write permission
The generated publish job grants id-token: write (required for OIDC) and then runs NuGet/login@v1 — a third-party action from the NuGet GitHub org — pinned only to a mutable semver tag, not a commit SHA. If that tag is ever updated to point at malicious code (via account compromise, tag mutation, or a future malicious maintainer), the action runs with the job's OIDC token in scope and can request a GitHub Actions JWT, exchange it with nuget.org for a temporary API key, and publish arbitrary packages under the victim's package name. Because id-token: write is not present in the non-OIDC path, this risk is entirely new in this PR. The same mutable-tag problem affects actions/checkout@v6 and actions/setup-dotnet@v5 in the same job, but those are GitHub's own actions and carry lower supply-chain risk. NuGet/login is a third-party action and is the highest-risk unpinned reference here.
Prompt To Fix With AI
In the Eta template `generators/csharp/base/src/asIs/github-ci.yml`, replace the mutable `NuGet/login@v1` tag with a pinned commit SHA. Look up the current HEAD commit of https://github.com/NuGet/login for tag v1 (e.g. `NuGet/login@<full-40-char-sha> # v1`) and use that. Also consider pinning `actions/checkout` and `actions/setup-dotnet` to their commit SHAs in the same `publish` job, since they run inside a job that already has `id-token: write`. Update the corresponding seed fixture `seed/csharp-sdk/exhaustive/oidc-token/.github/workflows/ci.yml` to match.Severity: medium | Confidence: 85% | React with 👍 if useful or 👎 if not
Docs Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on
Docs generation runs |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
SDK Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
* ci: publish to NuGet via Trusted Publishing (OIDC) Replace the long-lived NUGET_API_KEY publish step with nuget.org Trusted Publishing. A dedicated publish job (needs: [ci]) mints a short-lived key via NuGet/login@v1 using GitHub OIDC, scoping id-token: write to publishing only (off the test-running job). Mirrors the OIDC workflow now generated first-class by fern-csharp-sdk 2.69.0 (fern-api/fern#16725), adapted to this repo's solution build, dotnet 8.x, and legacy-tag exclusions. ci.yml stays in .fernignore so these customizations are preserved. Requires a nuget.org trust policy (owner=square, repo=square-dotnet-sdk, workflow=ci.yml) and a NUGET_USER secret; the NUGET_API_KEY secret can be removed after the first successful publish. * ci: pin NuGet/login to commit SHA (zizmor unpinned-uses) * ci: pass OIDC key via env to avoid run-step interpolation (semgrep) --------- Co-authored-by: jsklan <judah@buildwithfern.com>
Description
Adds NuGet Trusted Publishing (OIDC) support to the C# SDK generator as a config-gated publish strategy alongside the existing API-key path. Mirrors the npm/PyPI OIDC pattern already in the codebase.
Changes Made
Config detection (
getGeneratorConfig.ts)When
api-key: OIDC(or<USE_OIDC>) is set ingenerators.ymlfor a NuGet output, the CLI passesapiKeyEnvironmentVariable: "<USE_OIDC>"andshouldGeneratePublishWorkflow: truethrough the IR — same pattern as npm/PyPI OIDC.nuget: (value) => { + const apiKey = (value.apiKey ?? "").trim(); + const useOidc = apiKey === "<USE_OIDC>" || apiKey === "OIDC"; return FernGeneratorExec.GithubPublishInfo.nuget({ - apiKeyEnvironmentVariable: EnvironmentVariable(...) + apiKeyEnvironmentVariable: EnvironmentVariable(useOidc ? "<USE_OIDC>" : ...), + shouldGeneratePublishWorkflow: useOidc ? true : undefined }); }Workflow template (
github-ci.yml)OIDC path emits a separate
publishjob withpermissions: {contents: read, id-token: write},NuGet/login@v1for OIDC token exchange, and--skip-duplicateon push. API-key path is unchanged.Template rendering (
CsharpProject.ts)Detects
apiKeyEnvironmentVariable === "<USE_OIDC>"→ passesuseOidc: trueto the Eta template. When OIDC,nugetTokenEnvvarfalls back to"NUGET_API_TOKEN"(unused by the OIDC path but keeps template consistent).Setup docs
OIDC workflow includes a comment block at the top with trust-policy setup instructions (create policy on nuget.org, set
NUGET_USERsecret, filename contract note, private-repo 7-day activation window, remove stale API key).Unit tests (
getGithubPublishConfig.test.ts)8 test cases covering:
"OIDC"and"<USE_OIDC>"→ trusted publishing,"${SECRET}"→ secret ref, empty/undefined → no key, whitespace trimming, plain literal → empty env var,undefinedinput →undefinedoutput. Plus 1 test verifying npm OIDC parity.Seed test fixture
Added
oidc-tokenvariant to theexhaustivefixture (seed.yml) withapiKey: OIDC, generating a complete OIDC workflow snapshot.Testing
getGithubPublishConfig.test.ts, all passing)exhaustive/oidc-token/) verifiedexhaustive/explicit-namespaces/) unchanged (only trailing blank line removed from Eta-%>fix)pnpm run check(lint + typecheck) passingLink to Devin session: https://app.devin.ai/sessions/f98773cc2b234786a01c06f58022150a