Files
Miha Kralj 9d8cd22ae6 fix: add environment 'pypi' to Publish_Package job for trusted publishing
PyPI trusted publisher OIDC token exchange failed because the workflow
job did not declare a GitHub environment. PyPI's trusted publisher config
requires environment claim to match. Added environment: pypi to the
Publish_Package job in Release.yml.
2026-03-03 13:15:48 -08:00

360 lines
12 KiB
YAML

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"