Skip to content

feat(csharp): add NuGet trusted publishing (OIDC) support#16725

Merged
Swimburger merged 3 commits into
mainfrom
devin/1782419805-nuget-oidc-support
Jun 25, 2026
Merged

feat(csharp): add NuGet trusted publishing (OIDC) support#16725
Swimburger merged 3 commits into
mainfrom
devin/1782419805-nuget-oidc-support

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

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 in generators.yml for a NuGet output, the CLI passes apiKeyEnvironmentVariable: "<USE_OIDC>" and shouldGeneratePublishWorkflow: true through 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 publish job with permissions: {contents: read, id-token: write}, NuGet/login@v1 for OIDC token exchange, and --skip-duplicate on push. API-key path is unchanged.

# OIDC publish job (when useOidc=true)
publish:
  needs: [ci]
  permissions:
    contents: read
    id-token: write
  steps:
    # checkout, setup-dotnet, build, pack ...
    - uses: NuGet/login@v1
      id: login
      with:
        user: ${{ secrets.NUGET_USER }}
    - run: dotnet nuget push ... --api-key ${{ steps.login.outputs.NUGET_API_KEY }}

Template rendering (CsharpProject.ts)

Detects apiKeyEnvironmentVariable === "<USE_OIDC>" → passes useOidc: true to the Eta template. When OIDC, nugetTokenEnvvar falls 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_USER secret, 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, undefined input → undefined output. Plus 1 test verifying npm OIDC parity.

Seed test fixture

Added oidc-token variant to the exhaustive fixture (seed.yml) with apiKey: OIDC, generating a complete OIDC workflow snapshot.

Testing

  • Unit tests added (9 tests in getGithubPublishConfig.test.ts, all passing)
  • Seed test snapshot for OIDC path (exhaustive/oidc-token/) verified
  • Existing api-key fixture (exhaustive/explicit-namespaces/) unchanged (only trailing blank line removed from Eta -%> fix)
  • pnpm run check (lint + typecheck) passing

Link to Devin session: https://app.devin.ai/sessions/f98773cc2b234786a01c06f58022150a

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-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

run: dotnet pack <%= libraryProjectFilePath %> --no-build --no-restore -c Release

- name: NuGet login (OIDC → temp API key)
uses: NuGet/login@v1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-06-25T05:19:47Z).

Fixture main PR Delta
docs 220.4s (n=5) 252.7s (35 versions) +32.3s (+14.7%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-06-25T05:19:47Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-06-25 22:14 UTC

Swimburger and others added 2 commits June 25, 2026 21:42
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>
@github-actions

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-06-25T05:19:47Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 71s (n=5) 109s (n=5) 70s -1s (-1.4%)
go-sdk square 134s (n=5) 284s (n=5) 131s -3s (-2.2%)
java-sdk square 213s (n=5) 282s (n=5) 223s +10s (+4.7%)
php-sdk square 59s (n=5) 84s (n=5) 53s -6s (-10.2%)
python-sdk square 136s (n=5) 241s (n=5) 133s -3s (-2.2%)
ruby-sdk-v2 square 91s (n=5) 129s (n=5) 88s -3s (-3.3%)
rust-sdk square 183s (n=5) 173s (n=5) 155s -28s (-15.3%)
swift-sdk square 57s (n=5) 431s (n=5) 51s -6s (-10.5%)
ts-sdk square 236s (n=5) 239s (n=5) 227s -9s (-3.8%)

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 fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-06-25T05:19:47Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-06-25 22:16 UTC

@Swimburger Swimburger merged commit a49ef96 into main Jun 25, 2026
219 checks passed
@Swimburger Swimburger deleted the devin/1782419805-nuget-oidc-support branch June 25, 2026 23:10
mikekono pushed a commit to square/square-dotnet-sdk that referenced this pull request Jun 26, 2026
* 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>
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