name: Publish Workflow on: push: paths-ignore: - '**.md' - 'docs/**' - '.gitignore' - 'LICENSE' pull_request: paths-ignore: - '**.md' - 'docs/**' - '.gitignore' - 'LICENSE' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: {} env: DOTNET_VERSION: '10.x' DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true jobs: # ============================================================================== # 1. Build & Test (Generates Coverage) # ============================================================================== Build_Test_Coverage: runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Cache NuGet packages uses: actions/cache@v4 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} restore-keys: ${{ runner.os }}-nuget- - name: Restore dependencies run: dotnet restore - name: Build run: dotnet build --no-restore --configuration Debug - name: Collect SARIF Files run: | mkdir -p sarif-reports find . -name "roslyn.sarif" -type f -exec cp {} sarif-reports/ \; # Rename SARIF files to include project name cd sarif-reports count=1 for file in roslyn.sarif*; do if [ -f "$file" ]; then mv "$file" "roslyn_${count}.sarif" count=$((count+1)) fi done ls -la - name: Upload SARIF Artifacts uses: actions/upload-artifact@v4 with: name: sarif-reports path: sarif-reports/ - name: Run Tests run: | dotnet test --no-build --configuration Debug \ --collect:"XPlat Code Coverage" \ --results-directory:./TestResults \ -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover - name: Collect Coverage Files run: | mkdir -p coverage count=1 find TestResults -name "coverage.opencover.xml" | while read file; do cp "$file" "coverage/coverage_${count}.opencover.xml" count=$((count+1)) done - name: Debug Coverage Paths run: | echo "=== Coverage file paths ===" grep -h "fullPath=" coverage/*.xml | head -20 || true - name: Upload Coverage Artifacts uses: actions/upload-artifact@v4 with: name: coverage-reports path: coverage/ # ============================================================================== # 2. SonarCloud Analysis # ============================================================================== Sonar_Analysis: needs: Build_Test_Coverage runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: 17 distribution: 'zulu' - name: Cache NuGet packages uses: actions/cache@v4 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} restore-keys: ${{ runner.os }}-nuget- - name: Download Coverage Reports uses: actions/download-artifact@v4 with: name: coverage-reports path: coverage - name: Install SonarScanner run: | dotnet tool install --global dotnet-sonarscanner --version 9.0.0 echo "$HOME/.dotnet/tools" >> $GITHUB_PATH - name: Run SonarCloud Analysis env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} shell: bash run: | set -e # Fail immediately if any command exits with a non-zero status if [ -z "$SONAR_TOKEN" ]; then echo "WARNING: SONAR_TOKEN is not set. Skipping analysis." exit 0 fi echo "SONAR_TOKEN is set (Length: ${#SONAR_TOKEN})" dotnet restore # Added verbose logging to debug 9.0.0 auth issues # Reverted to sonar.token since the token has been updated # Removed quotes from /k and /o to ensure they are parsed correctly dotnet sonarscanner begin /k:mihakralj_QuanTAlib /o:mihakralj-quantalib \ /d:sonar.token="$SONAR_TOKEN" \ /d:sonar.host.url="https://sonarcloud.io" \ /d:sonar.cs.opencover.reportsPaths="coverage/*.xml" \ /d:sonar.coverage.exclusions="**Tests.cs,**/*.md,**/*.html,**/*.css,**/docs/**/*,**/archive/**/*,**/notebooks/**/*,**/obj/**/*,**/bin/**/*" \ /d:sonar.exclusions="**/TestResults/**/*,**/bin/**/*,**/obj/**/*,**/*.html,**/coverage/**/*,**/CoverageReport/**/*,**/*.md,**/*.css,**/docs/**/*,**/archive/**/*,**/notebooks/**/*" \ /d:sonar.test.exclusions="**Tests.cs,**/obj/**/*,**/bin/**/*" \ /d:sonar.scanner.scanAll="false" dotnet build --no-restore --configuration Debug dotnet sonarscanner end /d:sonar.token="$SONAR_TOKEN" # ============================================================================== # 3. Qodana Scan # ============================================================================== Qodana_Scan: needs: Build_Test_Coverage runs-on: ubuntu-latest permissions: contents: read checks: write security-events: write pull-requests: read steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download Coverage Reports uses: actions/download-artifact@v4 with: name: coverage-reports path: .qodana/code-coverage - name: Debug Coverage Paths run: | echo "=== Coverage XML paths sample ===" grep -h 'fullPath=' .qodana/code-coverage/*.xml | head -10 echo "" echo "=== Project structure ===" find . -name "*.cs" -type f | grep -E "lib/|quantower/" | head -10 - name: Qodana Scan uses: JetBrains/qodana-action@v2024.3.4 env: QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} with: args: --coverage-dir,.qodana/code-coverage # ============================================================================== # 5. Codacy Coverage Upload # ============================================================================== Codacy_Coverage_Upload: needs: Build_Test_Coverage runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download Coverage Reports uses: actions/download-artifact@v4 with: name: coverage-reports path: coverage - name: Upload Coverage to Codacy env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} run: | if [ -z "$CODACY_PROJECT_TOKEN" ]; then echo "CODACY_PROJECT_TOKEN is not set. Skipping upload." exit 0 fi # Download the reporter script curl -Ls https://coverage.codacy.com/get.sh -o codacy.sh chmod +x codacy.sh # Upload each file for file in coverage/*.xml; do echo "Uploading $file..." ./codacy.sh report -r "$file" --partial done # Finalize ./codacy.sh final continue-on-error: true # ============================================================================== # 6. Codacy SARIF Upload # ============================================================================== Codacy_SARIF_Upload: needs: Build_Test_Coverage runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download SARIF Reports uses: actions/download-artifact@v4 with: name: sarif-reports path: sarif-reports - name: Upload SARIF to Codacy env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} run: | if [ -z "$CODACY_PROJECT_TOKEN" ]; then echo "CODACY_PROJECT_TOKEN is not set. Skipping upload." exit 0 fi echo "Installing Codacy Analysis CLI..." curl -L https://github.com/codacy/codacy-analysis-cli/releases/latest/download/codacy-analysis-cli-linux -o codacy-analysis-cli chmod +x codacy-analysis-cli # Upload each SARIF file for file in sarif-reports/*.sarif; do if [ -f "$file" ]; then echo "Uploading SARIF: $file" ./codacy-analysis-cli upload-results \ --provider gh \ --owner mihakralj \ --repository QuanTAlib \ --commit ${{ github.sha }} \ --upload-batch-size 50000 \ --tool roslyn \ --results-file "$file" || echo "Warning: Failed to upload $file" fi done continue-on-error: true # ============================================================================== # 6. Snyk Scan # ============================================================================== Snyk_Scan: runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Install Snyk CLI uses: snyk/actions/setup@806182742461562b67788a64410098c9d9b96adb # master - name: Restore dependencies run: dotnet restore - name: Run Snyk on Solution continue-on-error: true env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} run: snyk test --all-projects # ============================================================================== # 7. CodeQL Analysis # ============================================================================== CodeQL_Analysis: runs-on: ubuntu-latest permissions: security-events: write actions: read contents: read steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: 'csharp' queries: security-and-quality config-file: ./.github/codeql/codeql-config.yml - name: Build run: dotnet build --configuration Debug - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 with: output: results upload: true # ============================================================================== # 8. Publish Package # ============================================================================== Publish_Package: needs: [Build_Test_Coverage, Sonar_Analysis, Qodana_Scan, Codacy_Coverage_Upload, Codacy_SARIF_Upload, Snyk_Scan, CodeQL_Analysis] if: >- (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || endsWith(github.ref, '-dev'))) || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Install GitVersion uses: gittools/actions/gitversion/setup@v4.2.0 with: versionSpec: '6.x' includePrerelease: true - name: Determine Version id: gitversion uses: gittools/actions/gitversion/execute@v4.2.0 with: updateAssemblyInfo: false - name: Build & Pack run: | dotnet restore dotnet build ./lib/quantalib.csproj --configuration Release --nologo dotnet build ./quantower/Momentum.csproj --configuration Release --nologo dotnet build ./quantower/Trends.csproj --configuration Release --nologo dotnet build ./quantower/Volume.csproj --configuration Release --nologo - name: Create Development Release if: github.ref == 'refs/heads/dev' || endsWith(github.ref, '-dev') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release delete development --yes || true gh release create development \ --title "Development Build" \ --notes "Latest development build from commit ${{ github.sha }}" \ --prerelease \ --target ${{ github.sha }} \ lib/bin/Release/net10.0/QuanTAlib.dll \ quantower/bin/Release/Momentum.dll \ quantower/bin/Release/Trends.dll \ quantower/bin/Release/Volume.dll - name: Push to MyGet if: github.ref == 'refs/heads/dev' || endsWith(github.ref, '-dev') continue-on-error: true run: | dotnet nuget push 'lib/bin/Release/QuanTAlib.*.nupkg' \ --source https://www.myget.org/F/quantalib/api/v3/index.json \ --force-english-output \ --api-key ${{ secrets.MYGET_DEPLOY_KEY_QUANTALIB }} - name: Create GitHub Release if: github.ref == 'refs/heads/main' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release create v${{ steps.gitversion.outputs.MajorMinorPatch }} \ --title "Release from commit ${{ steps.gitversion.outputs.MajorMinorPatch }}" \ --notes "Release notes for this version." \ quantower/bin/Release/Momentum.dll \ quantower/bin/Release/Trends.dll \ quantower/bin/Release/Volume.dll - name: Push to NuGet if: ${{ github.ref == 'refs/heads/main' }} run: | dotnet nuget push 'lib/bin/Release/QuanTAlib.*.nupkg' \ --source https://api.nuget.org/v3/index.json \ --skip-duplicate \ --api-key ${{ secrets.NUGET_DEPLOY_KEY_QUANTLIB }}