diff --git a/.github/workflows/publish_nuget.yml b/.github/workflows/publish_nuget.yml index 8d5d129..9a72bc0 100644 --- a/.github/workflows/publish_nuget.yml +++ b/.github/workflows/publish_nuget.yml @@ -8,11 +8,13 @@ on: permissions: contents: write + id-token: write # Required for NuGet trusted publishing (OIDC token exchange). jobs: publish-nuget: name: publish-nuget runs-on: ubuntu-latest + environment: nuget # Must match the environment named in the nuget.org Trusted Publisher policy. steps: - name: "Checkout" @@ -41,8 +43,29 @@ jobs: - name: "dotnet pack" run: dotnet pack /p:PackageVersion=${{ env.PACKAGE_VERSION }} -c Release -o ./output + - name: "NuGet login (OIDC)" + # Exchanges the workflow's OIDC token for a short-lived NuGet API key + # under the Trusted Publisher policy configured on nuget.org. + # Requires: + # - permissions.id-token: write at the workflow level (above) + # - environment: nuget on this job (matches the policy) + # - NUGET_USER repo secret = the package owner's nuget.org username + # See https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing + uses: NuGet/login@v1 + id: nuget-login + with: + user: ${{ secrets.NUGET_USER }} + - name: "dotnet nuget push" - run: dotnet nuget push "output/*.nupkg" -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate + run: | + dotnet nuget push "output/*.nupkg" \ + --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} \ + --source https://api.nuget.org/v3/index.json \ + --skip-duplicate + dotnet nuget push "output/*.snupkg" \ + --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} \ + --source https://api.nuget.org/v3/index.json \ + --skip-duplicate - name: "Extract latest release notes" shell: pwsh @@ -64,7 +87,9 @@ jobs: name: "ShellSyntaxTree ${{ github.ref_name }}" tag_name: ${{ github.ref_name }} body_path: RELEASE_NOTES_LATEST.md - files: output/*.nupkg + files: | + output/*.nupkg + output/*.snupkg draft: false prerelease: ${{ contains(github.ref_name, '-') }} env: diff --git a/TOOLING.md b/TOOLING.md index 14207e8..8f17fff 100644 --- a/TOOLING.md +++ b/TOOLING.md @@ -41,9 +41,38 @@ parses to its declared `expected` AST. |---|---|---| | `NuGet.Config` | repo root | feed configuration (currently `nuget.org`) | | `dotnet pack` | shell | produces `.nupkg` and `.snupkg` (symbol package) | -| `dotnet nuget push` | CI only (`NUGET_KEY` secret) | publishes to `nuget.org` on tag | +| `dotnet nuget push` | CI only (`NuGet/login@v1` OIDC) | publishes to `nuget.org` on tag | | Central Package Management | `Directory.Packages.props` | single source of truth for package versions | +### NuGet trusted publishing + +`publish_nuget.yml` uses +[trusted publishing](https://learn.microsoft.com/en-us/nuget/nuget-org/trusted-publishing) +— no long-lived API key in repo secrets. The workflow exchanges its +OIDC token for a short-lived API key via the `NuGet/login@v1` action. + +Required configuration (one-time, on `nuget.org`): + +1. Sign in to nuget.org → **Account → Trusted Publisher Policies** → + **Add new policy**. +2. Publisher: GitHub Actions. Repository owner: `Aaronontheweb`. + Repository: `ShellSyntaxTree`. Workflow file: `publish_nuget.yml`. + Environment: `nuget`. Optional package-name pattern: + `ShellSyntaxTree*`. + +Required configuration (one-time, in repo settings): + +1. **Settings → Environments → New environment** → name `nuget`. + Optional protection: restrict deployment to tags matching + `v*.*.*`. +2. **Settings → Secrets and variables → Actions → New repository + secret** — `NUGET_USER` set to the nuget.org account username + that owns the package. The legacy `NUGET_KEY` secret can be + deleted once trusted publishing is verified. + +The workflow's `permissions: id-token: write` and `environment: nuget` +declarations are required for the OIDC exchange to succeed. + ## Source-Level Conventions - `Directory.Build.props` enforces: `Nullable=enable`, `LangVersion=latest`,