From 3fb7297620e91a55b517ce6df417a9e798f493b7 Mon Sep 17 00:00:00 2001 From: Philippe Matray Date: Mon, 27 Jul 2026 19:36:28 +0200 Subject: [PATCH] ci: publish to NuGet via Trusted Publishing, not a long-lived key 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 --- .github/workflows/main.yml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8fc5d81..67cc803 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -124,6 +124,13 @@ jobs: if: github.event_name == 'release' runs-on: ubuntu-latest needs: [ validate_nuget, run_test ] + + # Job-level, so it must restate contents: read — a job-level block REPLACES the + # workflow-level one rather than adding to it. + permissions: + contents: read + id-token: write # request the GitHub OIDC token for NuGet Trusted Publishing + steps: # Download the NuGet package created in the previous job - uses: actions/download-artifact@v8 @@ -140,8 +147,21 @@ jobs: # Publish all NuGet packages to NuGet.org # Use --skip-duplicate to prevent errors if a package with the same version already exists. # If you retry a failed workflow, already published packages will be skipped without error. + # Trusted Publishing: exchanges the OIDC token (needs `id-token: write` above) for a NuGet + # key valid ~1 hour, so no long-lived secret is stored. Keep it adjacent to the push so the + # key cannot expire in between. Requires a Trusted Publishing policy on nuget.org naming + # this repository and `main.yml`, plus the NUGET_USER secret (the nuget.org profile name). + # Deliberately UNGUARDED: this job only runs on a published Release, so publishing is + # expected. A missing policy must fail loudly rather than skip and leave the release + # unpublished behind a green check. + - name: NuGet login (OIDC -> short-lived key) + id: nuget-login + uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0 + with: + user: ${{ secrets.NUGET_USER }} + - name: Publish NuGet package run: | foreach($file in (Get-ChildItem "${{ env.NuGetDirectory }}" -Recurse -Include *.nupkg)) { - dotnet nuget push $file --api-key "${{ secrets.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push $file --api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate } \ No newline at end of file