From a4bdf4a7a048a52e6a9b05c634a457330f9cf905 Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Thu, 29 Oct 2026 17:03:03 -0300 Subject: [PATCH 1/2] Add GitHub workflow to build and publish the NuGet package - Add build_push_nuget.sh, which packs the QuantConnect.pythonnet NuGet and symbol packages and pushes them when NUGET_API_KEY is set - Add a workflow that runs it on published releases, using the release tag as the version and nuget.org Trusted Publishing (OIDC) instead of a long-lived API key. Manual runs only build the package - Remove the upstream monthly preview workflow, which has been failing on every scheduled run and never published a package - Exclude dot-files from the packed pythonnet/ content so the nuspec does not list the .gitignore and .gitkeep files NuGet leaves out - Keep *.sh files with LF line endings Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 1 + .github/workflows/nuget-preview.yml | 63 ----------------------------- .github/workflows/publish.yml | 47 +++++++++++++++++++++ build_push_nuget.sh | 35 ++++++++++++++++ src/runtime/Python.Runtime.csproj | 2 +- 5 files changed, 84 insertions(+), 64 deletions(-) create mode 100644 .gitattributes delete mode 100644 .github/workflows/nuget-preview.yml create mode 100644 .github/workflows/publish.yml create mode 100755 build_push_nuget.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..dfdb8b771 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/.github/workflows/nuget-preview.yml b/.github/workflows/nuget-preview.yml deleted file mode 100644 index d27382ad4..000000000 --- a/.github/workflows/nuget-preview.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: NuGet Preview Release - -on: - schedule: - - cron: "5 4 3 */1 *" # once a month, at 4:05 on 3rd - workflow_dispatch: - -jobs: - release: - name: Release Preview - runs-on: ubuntu-latest - environment: NuGet - timeout-minutes: 10 - - env: - PYTHONNET_SHUTDOWN_MODE: Normal - - steps: - - name: Get Date - run: | - echo "DATE_VER=$(date "+%Y-%m-%d")" >> $GITHUB_ENV - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.0.x' - - - name: Set up Python 3.8 - uses: actions/setup-python@v5 - with: - python-version: 3.8 - architecture: x64 - - - name: Install dependencies - run: | - pip install --upgrade -r requirements.txt - pip install numpy # for tests - - - name: Build and Install - run: | - pip install -v . - - - name: Set Python DLL path (non Windows) - if: ${{ matrix.os != 'windows' }} - run: | - python -m pythonnet.find_libpython --export >> $GITHUB_ENV - - - name: Python Tests - run: pytest - - - name: Embedding tests - run: dotnet test --runtime any-ubuntu src/embed_tests/ - - - name: Pack - run: dotnet pack --configuration Release --version-suffix preview${{env.DATE_VER}} --output "Release-Preview" - - - name: Publish NuGet - run: | - dotnet nuget push --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_MONTHLY }} Release-Preview/*.nupkg - dotnet nuget push --skip-duplicate --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_MONTHLY }} Release-Preview/*.snupkg diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..073c6f448 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,47 @@ +name: Publish NuGet + +on: + # Publishing a GitHub release builds and pushes the package, using the release tag as the version (e.g. 2.0.68) + release: + types: [published] + # Manual runs only build the package and attach it to the run, nothing is pushed + workflow_dispatch: + inputs: + version: + description: Package version, defaults to the csproj version + required: false + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # lets NuGet/login exchange the GitHub OIDC token for a short-lived nuget.org API key + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.x + + # The temporary API key expires after 1 hour, plenty for the build below + - name: NuGet login + if: github.event_name == 'release' + id: login + uses: NuGet/login@v1 + with: + user: ${{ secrets.NUGET_USER }} + + # Only pushes when NUGET_API_KEY is set, which is the case for releases + - name: Build and push + env: + VERSION: ${{ github.event.release.tag_name || inputs.version }} + NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }} + run: ./build_push_nuget.sh "$VERSION" + + - uses: actions/upload-artifact@v7 + with: + name: nuget + path: artifacts/* diff --git a/build_push_nuget.sh b/build_push_nuget.sh new file mode 100755 index 000000000..df00496c2 --- /dev/null +++ b/build_push_nuget.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Builds Python.Runtime, packs the QuantConnect.pythonnet NuGet and symbol packages into ./artifacts +# and pushes them to nuget.org when NUGET_API_KEY is set +# +# Usage: ./build_push_nuget.sh [version] +# version: package version, e.g. 2.0.68 or v2.0.68. Defaults to the in Python.Runtime.csproj. +# +# Requires git and the .NET 10 SDK. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT="$ROOT/src/runtime/Python.Runtime.csproj" +OUTPUT="$ROOT/artifacts" + +VERSION="${1:-}" +VERSION="${VERSION#v}" +if [[ -n "$VERSION" && ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid version '$1', expected e.g. 2.0.68" >&2 + exit 1 +fi + +# Everything under pythonnet/ is packed as content, so drop stale build outputs and __pycache__ first +git -C "$ROOT" clean -fdXq -- pythonnet + +echo "Packing NuGet package ${VERSION:-with the csproj version}" +rm -rf "$OUTPUT" +dotnet pack "$PROJECT" --configuration Release --output "$OUTPUT" ${VERSION:+-p:Version=$VERSION} + +ls "$OUTPUT" + +if [[ -n "${NUGET_API_KEY:-}" ]]; then + echo "Pushing to nuget.org" + # Also pushes the .snupkg symbol package next to it + dotnet nuget push "$OUTPUT"/*.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json +fi diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index e290143d9..5eb146aed 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -35,7 +35,7 @@ contentFiles/any/any/ true - + contentFiles/any/any/pythonnet true From 0bcb10fac8a569704178e0d52b0023274d7b7a3e Mon Sep 17 00:00:00 2001 From: Martin Molinero Date: Thu, 29 Oct 2026 17:33:58 -0300 Subject: [PATCH 2/2] Publish the NuGet package from a manual workflow run Use the version in the csproj instead of a release tag, so publishing is a single manual run of the workflow from master. nuget.org rejects versions that already exist, so a run without a version bump fails. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 24 +++++------------------- build_push_nuget.sh | 16 +++------------- 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 073c6f448..4d36e715e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,18 +1,12 @@ name: Publish NuGet -on: - # Publishing a GitHub release builds and pushes the package, using the release tag as the version (e.g. 2.0.68) - release: - types: [published] - # Manual runs only build the package and attach it to the run, nothing is pushed - workflow_dispatch: - inputs: - version: - description: Package version, defaults to the csproj version - required: false +# Run manually to build the package with the version in Python.Runtime.csproj and push it to nuget.org +on: workflow_dispatch jobs: publish: + # Only publish what is merged, a pushed version can't be replaced + if: github.ref == 'refs/heads/master' runs-on: ubuntu-latest permissions: contents: read @@ -28,20 +22,12 @@ jobs: # The temporary API key expires after 1 hour, plenty for the build below - name: NuGet login - if: github.event_name == 'release' id: login uses: NuGet/login@v1 with: user: ${{ secrets.NUGET_USER }} - # Only pushes when NUGET_API_KEY is set, which is the case for releases - name: Build and push env: - VERSION: ${{ github.event.release.tag_name || inputs.version }} NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }} - run: ./build_push_nuget.sh "$VERSION" - - - uses: actions/upload-artifact@v7 - with: - name: nuget - path: artifacts/* + run: ./build_push_nuget.sh diff --git a/build_push_nuget.sh b/build_push_nuget.sh index df00496c2..889cb9742 100755 --- a/build_push_nuget.sh +++ b/build_push_nuget.sh @@ -1,9 +1,6 @@ #!/usr/bin/env bash # Builds Python.Runtime, packs the QuantConnect.pythonnet NuGet and symbol packages into ./artifacts -# and pushes them to nuget.org when NUGET_API_KEY is set -# -# Usage: ./build_push_nuget.sh [version] -# version: package version, e.g. 2.0.68 or v2.0.68. Defaults to the in Python.Runtime.csproj. +# with the in Python.Runtime.csproj and pushes them to nuget.org when NUGET_API_KEY is set # # Requires git and the .NET 10 SDK. set -euo pipefail @@ -12,19 +9,12 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT="$ROOT/src/runtime/Python.Runtime.csproj" OUTPUT="$ROOT/artifacts" -VERSION="${1:-}" -VERSION="${VERSION#v}" -if [[ -n "$VERSION" && ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then - echo "Invalid version '$1', expected e.g. 2.0.68" >&2 - exit 1 -fi - # Everything under pythonnet/ is packed as content, so drop stale build outputs and __pycache__ first git -C "$ROOT" clean -fdXq -- pythonnet -echo "Packing NuGet package ${VERSION:-with the csproj version}" +echo "Packing NuGet package" rm -rf "$OUTPUT" -dotnet pack "$PROJECT" --configuration Release --output "$OUTPUT" ${VERSION:+-p:Version=$VERSION} +dotnet pack "$PROJECT" --configuration Release --output "$OUTPUT" ls "$OUTPUT"