name: Release Workflow on: workflow_dispatch: permissions: {} defaults: run: shell: bash env: DOTNET_VERSION: "10.x" PYTHON_VERSION: "3.14" DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true DOTNET_NOLOGO: true jobs: # ============================================================================== # Build Python wheels (parallel matrix) # ============================================================================== Build_Python_Wheels: runs-on: ${{ matrix.os }} timeout-minutes: 30 permissions: contents: read strategy: fail-fast: false matrix: include: - wheel_id: win-amd64 os: windows-latest rid: win-x64 native_dir: win_amd64 native_file: quantalib_native.dll wheel_platform_tag: win_amd64 - wheel_id: win-arm64 os: windows-latest rid: win-arm64 native_dir: win_arm64 native_file: quantalib_native.dll wheel_platform_tag: win_arm64 - wheel_id: linux-amd64 os: ubuntu-latest rid: linux-x64 native_dir: linux_x86_64 native_file: quantalib_native.so wheel_platform_tag: manylinux_2_17_x86_64.manylinux2014_x86_64 - wheel_id: linux-arm64 os: ubuntu-24.04-arm rid: linux-arm64 native_dir: linux_arm64 native_file: quantalib_native.so wheel_platform_tag: manylinux_2_17_aarch64.manylinux2014_aarch64 - wheel_id: macos-amd64 os: macos-15-intel rid: osx-x64 native_dir: macosx_x86_64 native_file: quantalib_native.dylib wheel_platform_tag: macosx_10_13_x86_64 - wheel_id: macos-arm64 os: macos-14 rid: osx-arm64 native_dir: macosx_arm64 native_file: quantalib_native.dylib wheel_platform_tag: macosx_11_0_arm64 steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Resolve version id: version shell: pwsh run: | $version = (Get-Content "lib/VERSION" -Raw).Trim() if ([string]::IsNullOrWhiteSpace($version)) { Write-Error "lib/VERSION is empty" exit 1 } "value=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append "Using version: $version" - name: Restore dependencies run: dotnet restore - name: Publish python native wrapper shell: pwsh run: | $nativeOut = "./python/quantalib/native/${{ matrix.native_dir }}" dotnet publish ./python/python.csproj ` -c Release ` -r ${{ matrix.rid }} ` --self-contained true ` -o $nativeOut ` -p:ContinuousIntegrationBuild=true ` -p:DebugSymbols=false ` -p:DebugType=none $expectedNative = Join-Path $nativeOut "${{ matrix.native_file }}" if (-not (Test-Path $expectedNative)) { Write-Error "Missing native library: $expectedNative" exit 1 } Get-ChildItem $nativeOut -File | Where-Object { $_.Name -ne "${{ matrix.native_file }}" } | Remove-Item -Force "Native payload files:" Get-ChildItem $nativeOut -File | ForEach-Object { $_.Name } - name: Set wheel version for development if: github.ref_name != 'main' shell: pwsh run: | $baseVersion = (Get-Content "lib/VERSION" -Raw).Trim() $devVersion = "$baseVersion.dev$env:GITHUB_RUN_NUMBER" Set-Content -Path "lib/VERSION" -Value $devVersion -NoNewline "Using dev wheel version: $devVersion" - name: Build wheel shell: pwsh run: | Remove-Item ./python/dist/*.whl -ErrorAction SilentlyContinue python -m pip install --upgrade pip build hatchling wheel python -m build --wheel ./python $wheel = Get-ChildItem ./python/dist/*.whl | Select-Object -First 1 if (-not $wheel) { Write-Error "No wheel produced by build" exit 1 } python -m wheel tags ` --python-tag py3 ` --abi-tag none ` --platform-tag ${{ matrix.wheel_platform_tag }} ` --remove ` "$($wheel.FullName)" $taggedWheel = Get-ChildItem ./python/dist/*.whl | Select-Object -First 1 if (-not $taggedWheel) { Write-Error "No wheel found after retagging" exit 1 } "Tagged wheel: $($taggedWheel.Name)" - name: Upload wheel artifact uses: actions/upload-artifact@v4 with: name: python-wheel-${{ matrix.wheel_id }} path: python/dist/*.whl retention-days: 7 # ============================================================================== # Publish Package (manual trigger only) # ============================================================================== Publish_Package: needs: [Build_Python_Wheels] runs-on: ubuntu-latest environment: pypi timeout-minutes: 15 permissions: contents: write id-token: write steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} cache: true cache-dependency-path: | **/packages.lock.json **/*.csproj **/*.sln - name: Resolve version id: version run: | VERSION="$(tr -d '[:space:]' < lib/VERSION)" if [ -z "$VERSION" ]; then echo "lib/VERSION is empty" exit 1 fi echo "value=$VERSION" >> "$GITHUB_OUTPUT" echo "Using version: $VERSION" - name: Restore dependencies run: dotnet restore - name: Build (warnings as errors) run: dotnet build ./QuanTAlib.slnx --configuration Release --no-restore --nologo /warnaserror - name: Test (core) run: dotnet test ./lib/QuanTAlib.Tests.csproj --configuration Release --no-build --nologo - name: Test (quantower) run: dotnet test ./quantower/Quantower.Tests.csproj --configuration Release --no-build --nologo - name: Pack NuGet run: | dotnet pack ./lib/quantalib.csproj \ --configuration Release \ --no-build \ -p:ContinuousIntegrationBuild=true \ -p:PackageVersion=${{ steps.version.outputs.value }} ls -lh lib/bin/Release/*.nupkg || true - name: Download all python wheel artifacts uses: actions/download-artifact@v4 with: pattern: python-wheel-* path: python-wheel-all merge-multiple: false - name: Merge wheel artifacts for package publishing run: | mkdir -p python-wheel for dir in python-wheel-all/python-wheel-*; do [ -d "$dir" ] || continue cp -v "$dir"/*.whl python-wheel/ done echo "Wheels prepared for package publishing:" ls -lh python-wheel/*.whl - name: Collect release assets run: | mkdir -p release-assets cp -v lib/bin/Release/*.nupkg release-assets/ cp -v python-wheel/*.whl release-assets/ if [ ! -f "quantower/bin/Release/quantower.dll" ]; then echo "quantower.dll not found at quantower/bin/Release/quantower.dll" echo "Available DLLs under quantower/bin:" find quantower/bin -type f -name "*.dll" -maxdepth 4 || true exit 1 fi cp -v quantower/bin/Release/quantower.dll release-assets/quantower_quantalib.dll echo "Release assets:" ls -lh release-assets - name: Detect release type id: release_type run: | BRANCH="${GITHUB_REF_NAME}" echo "Branch: $BRANCH" if [ "$BRANCH" = "main" ]; then echo "type=production" >> $GITHUB_OUTPUT echo "Detected: PRODUCTION release (main branch)" else echo "type=development" >> $GITHUB_OUTPUT echo "Detected: DEVELOPMENT release (branch=$BRANCH)" fi - name: Create Development Release if: steps.release_type.outputs.type == 'development' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release delete development --yes 2>/dev/null || true git push --delete origin refs/tags/development 2>/dev/null || true gh release create development \ --title "Development Build" \ --notes "Latest development build from commit ${{ github.sha }}" \ --prerelease \ --target ${{ github.sha }} \ release-assets/*.nupkg \ release-assets/*.whl \ release-assets/quantower_quantalib.dll - name: Publish wheel to TestPyPI if: steps.release_type.outputs.type == 'development' uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ packages-dir: python-wheel skip-existing: true verbose: true attestations: false - name: Push to MyGet if: steps.release_type.outputs.type == 'development' continue-on-error: true env: MYGET_API_KEY: ${{ secrets.MYGET_DEPLOY_KEY_QUANTALIB }} run: | if [ -z "${MYGET_API_KEY:-}" ]; then echo "MYGET_DEPLOY_KEY_QUANTALIB not set. Skipping." exit 0 fi dotnet nuget push 'lib/bin/Release/*.nupkg' \ --source https://www.myget.org/F/quantalib/api/v3/index.json \ --skip-duplicate \ --api-key "$MYGET_API_KEY" - name: Publish wheels to PyPI if: steps.release_type.outputs.type == 'production' uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: python-wheel skip-existing: true verbose: true attestations: false - name: Create Production Release if: steps.release_type.outputs.type == 'production' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release create "v${{ steps.version.outputs.value }}" \ --title "Release ${{ steps.version.outputs.value }}" \ --generate-notes \ release-assets/*.nupkg \ release-assets/*.whl \ release-assets/quantower_quantalib.dll - name: Push to NuGet if: steps.release_type.outputs.type == 'production' env: NUGET_API_KEY: ${{ secrets.NUGET_DEPLOY_KEY_QUANTLIB }} run: | if [ -z "${NUGET_API_KEY:-}" ]; then echo "NUGET_DEPLOY_KEY_QUANTLIB not set. Failing publish." exit 1 fi dotnet nuget push 'lib/bin/Release/*.nupkg' \ --source https://api.nuget.org/v3/index.json \ --skip-duplicate \ --api-key "$NUGET_API_KEY"