droidsolutions / semantic-release-nuget

Semantic Release plugin to create and publish NuGet packages.
MIT License
8 stars 1 forks source link

Publish to multiple sources/servers #627

Open BinToss opened 1 year ago

BinToss commented 1 year ago

This will require semantic-release-nuget's configuration object to be changed.

Currently, semantic-release-nuget only allows pushing to one custom server with NUGET_TOKEN. This prevents users from pushing to multiple servers e.g. NuGet.org and GitHub registries. This could be alleviated by a GitHub equivalent to publishToGitLab, but the problem still exists when an organization wants to publish their NuPkg(s) to both NuGet.org and a custom, private registry.

To allow users to push packages to multiple servers, server URLs and API tokens will need to be passed as an array of pairs i.e.

{
    "plugins": [
        ...
        [
            "@droidsolutions-oss/semantic-release-nuget",
            [
                "registries": [
                    [ "https://nuget.mycomapny.com/v3/index.json", "${PRIVATE_TOKEN}" ],
                    [ "https://npr.customserver.com/v3/index.json", "${OTHER_TOKEN}"]
                ]
            ]
        ],
        ...
    ],
}

Alternatively, an array of well-defined objects which could help with hints/suggestions in IDEs when semantic-release improves support for plugins' parameters.

"registries": [
    {
        "url": "https://nuget.mycomapny.com/v3/index.json",
        "token": "${PRIVATE_TOKEN}"
    },
    {
        "url": "https://npr.customserver.com/v3/index.json",
        "token": "${OTHER_TOKEN}"
    }
]
Kampfmoehre commented 1 year ago

I can take a look at this, but it probably needs more refinement. The reason publishToGitLab exists is, that the handling is a bit different there. I am not sure about GitHubs registry, but I guess it is similar using some special CI variables. We either must resolve env vars in the server url, or add some type property to it

{
  "registries": [
    {
      "url": "https://nuget.mycomapny.com/v3/index.json",
      "token": "${PRIVATE_TOKEN}"
    },
    {
      "url": "https://npr.customserver.com/v3/index.json",
      "token": "${OTHER_TOKEN}"
    },
    {
      "url": "${CI_SERVER_URL}/api/v4/projects/{CI_PROJECT_ID}/packages/nuget/index.json",
      "token": "${CI_JOB_TOKEN}",
      "type": "gitlab"
    }
  ]
}
BinToss commented 7 months ago

Currently, my release workflow is as follows:

# Test changes locally with https://github.com/nektos/act
# Design with graphs via https://marketplace.visualstudio.com/items?itemName=actionforge.actionforge
name: Release

on:
  push:
    branches:
      - main
      - develop
env:
  DOTNET_ROLL_FORWARD: "Major"
  DOTNET_CLI_TELEMETRY_OPTOUT: 1
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
  DOTNET_NOLOGO: 1

jobs:
  ci:
    name: CI
    uses: ./.github/workflows/ci.yml
  # based on https://github.com/semantic-release/semantic-release/blob/master/docs/recipes/ci-configurations/github-actions.md
  release:
    needs: [ci]
    runs-on: ubuntu-latest
    permissions:
      contents: write # to be able to publish a GitHub release
      issues: write # to be able to comment on released issues
      pull-requests: write # to be able to comment on released pull requests
      id-token: write # to enable use of OIDC for npm provenance
      packages: write # for pushing GitHub Nuget packages

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # necessary for semantic release

      - uses: actions/setup-node@v4
        with:
          cache: "npm"
          check-latest: true
          node-version-file: package.json
      - run: npm i -g npm@latest # required for attestation. There's a games-stopping bug in Node.js LTS's NPM.

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: "8.x"

      - name: NPM - Clean Install
        run: npm ci

      - name: NPM - Audit Signatures
        run: npm audit signatures

      - name: Update version in README's Avalonia badge
        shell: pwsh
        run: |
          $AV = [version]::new(((dotnet msbuild .\GroupBox.Avalonia\GroupBox.Avalonia.csproj  -getItem:PackageReference | ConvertFrom-Json).Items.PackageReference | Where-Object {$_.Identity -eq 'Avalonia'}).Version).ToString();
          $pattern = "(?<=\[!\[avalonia]\(https:\/\/img\.shields\.io\/badge\/avalonia-v)\d+\.\d+\.\d+";
          (Get-Content -Path README.md -Raw) -creplace $pattern, $AV | Set-Content -NoNewLine -Path README.md

      # [release#
      # steps](https://github.com/semantic-release/semantic-release#release-steps)
      # Plugins add sub-steps e.g. @semantic-release/git's Prepare will create a
      # release commit, including configurable file assets."
      # After the new version Git tagged, @semantic-release/exec runs
      # `dotnet publish`. @semantic-release/github adds the artifacts to the
      # GitHub Release.
      - name: Semantic Release
        id: semantic_release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }}
        shell: pwsh
        run: |
          $output=npx semantic-release
          if ($output.EndsWith('There are no relevant changes, so no new version is released.')) {
            echo "NoNewVersion=true" >> "$env:GITHUB_OUTPUT"
          }

      # https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-nuget-registry
      # https://github.com/actions/setup-dotnet/tree/v3/#setting-up-authentication-for-nuget-feeds
      - name: .NET - Pack n' Push NuPkg
        if: ${{!steps.semantic_release.outputs.NO_NEW_VERSION}}
        env:
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
          NUGET_TOKEN: ${{secrets.NUGET_TOKEN}}
        shell: pwsh
        run: |
          dotnet pack ./GroupBox.Avalonia/GroupBox.Avalonia.csproj -o ./publish/
          dotnet nuget push ./publish/*.nupkg --source https://api.nuget.org/v3/index.json --api-key $env:NUGET_TOKEN
          dotnet nuget push ./publish/*.nupkg --source https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json --api-key $env:GITHUB_TOKEN

*Some users use a Personal Access Token instead of the workflow's auto-token for edge-cases. They may need a reminder (README?) to update its permissions if they want to push NuGet packages to GitHub.


edit: the url of a workflow's GitHub Package Repository should default to...

if (process.env["GITHUB_REPOSITORY_OWNER"]) {
    const GithubPkgUrl= `https://nuget.pkg.github.com/${process.env["GITHUB_REPOSITORY_OWNER"]}/index.json`
}

or, using the syntax in your example config...

{
    "url": "https://nuget.pkg.github.com/${GITHUB_REPOSITORY_OWNER}/index.json",
    "token": "${GITHUB_TOKEN}"
}
BinToss commented 7 months ago

Regarding tokens, how would users go about assigning them to each registry via environment variables? CLI may need to be adjusted, too. Perhaps --registries with a comma-separated, A-B array (url,key,url,key)?

Your example contains strings such as ${CI_JOB_TOKEN}. Do you intend this to be a string from which the environment variable is derived? For example, given "token": "${GITHUB_TOKEN}"...

// assuming registries is mapped or forEach'ed...hmm...which registry NUGET_TOKEN apply to? If length is one, it's easy.
if (pluginConfig.registries[i].token && pluginConfig.registries[i].token.startsWith("${") {
    token = process.env[pluginConfig.registries[i].token.replace("${").replace("}")];
}
Kampfmoehre commented 7 months ago

Regarding tokens, how would users go about assigning them to each registry via environment variables?

The idea was, to specify credentials along with the NuGet source but instead of hardcoding them (which would be bad practice) allow to use environment variables that the plugin can resolve.

Your example contains strings such as ${CI_JOB_TOKEN}. Do you intend this to be a string from which the environment variable is derived?

${CI_JOB_TOKEN} would be resolved in the verify step and replaced with the content of that environment variable. In GitLab CI this is a variable filled by GitLab with a token with enough rights to be used for that specific case.

In your case it would look more like this

  "registries": [
    {
      "name": "default",
      // url could be omitted for official NuGet server
      "token": "${NUGET_TOKEN}"
    },
    {
      "name": "github-public",
      "url": "https://nuget.pkg.github.com/${GITHUB_REPOSITORY_OWNER}/index.json",
      "token": "${GITHUB_TOKEN}",
      "type": "github"
    }
  ]

Maybe it would be better to build the url automatically instead of allowing to use env vars in it by using the type?. Type could be something like