name: Analysis Workflow on: push: paths-ignore: - "**/*.md" - "**/*.pine" - "docs/**" - ".gitignore" - "LICENSE" pull_request: paths-ignore: - "**/*.md" - "**/*.pine" - "docs/**" - ".gitignore" - "LICENSE" workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: {} defaults: run: shell: bash env: DOTNET_VERSION: "10.x" DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true DOTNET_NOLOGO: true # IMPORTANT: analyze the same commit we report to Codacy CHECKOUT_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} COMMIT_UUID: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} jobs: # ============================================================================== # 1) ReSharper InspectCode -> SARIF artifact # ============================================================================== ReSharper_Analysis: runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: read actions: write security-events: write steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ env.CHECKOUT_REF }} - 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: Restore dependencies run: dotnet restore - name: Prepare SARIF directory run: mkdir -p .sarif - name: Install JetBrains ReSharper GlobalTools run: | dotnet tool update --global JetBrains.ReSharper.GlobalTools || \ dotnet tool install --global JetBrains.ReSharper.GlobalTools echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH" - name: Run ReSharper InspectCode (SARIF) run: | set +e jb inspectcode QuanTAlib.sln \ --format=sarif \ --output=.sarif/resharper.sarif rc=$? set -e # jb may return non-zero for findings/config; missing SARIF is the real failure signal if [ ! -f ".sarif/resharper.sarif" ]; then echo "ERROR: ReSharper SARIF not generated (exit code: $rc)" exit 1 fi echo "ReSharper SARIF generated (exit code: $rc)" ls -lh .sarif/resharper.sarif - name: Upload SARIF artifact uses: actions/upload-artifact@v4 with: name: sarif-resharper path: .sarif/resharper.sarif retention-days: 7 # ============================================================================== # 2) Snyk Security Scan -> SARIF artifact # ============================================================================== Snyk_Scan: runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read actions: write security-events: write steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ env.CHECKOUT_REF }} - 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: Install Snyk CLI uses: snyk/actions/setup@806182742461562b67788a64410098c9d9b96adb - name: Restore dependencies run: dotnet restore - name: Prepare SARIF directory run: mkdir -p .sarif - name: Run Snyk Security Scan (SARIF) env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} run: | if [ -z "${SNYK_TOKEN:-}" ]; then echo "SNYK_TOKEN not set. Skipping Snyk scan." exit 0 fi set +e snyk test --all-projects --sarif-file-output=.sarif/snyk.sarif rc=$? set -e # Snyk exit codes: 0 = no issues, 1 = issues found, >1 = error if [ $rc -gt 1 ]; then echo "ERROR: Snyk scan failed (exit code: $rc)" exit $rc fi if [ ! -f ".sarif/snyk.sarif" ]; then echo "ERROR: Snyk SARIF not generated" exit 1 fi echo "Snyk SARIF generated (exit code: $rc)" ls -lh .sarif/snyk.sarif - name: Upload SARIF artifact if: always() uses: actions/upload-artifact@v4 with: name: sarif-snyk path: .sarif/snyk.sarif retention-days: 7 if-no-files-found: warn # ============================================================================== # 3) Semgrep Security Scan -> SARIF artifact # ============================================================================== Semgrep_Scan: runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read actions: write security-events: write steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ env.CHECKOUT_REF }} - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.x" - name: Prepare SARIF directory run: mkdir -p .sarif - name: Run Semgrep (SARIF) env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} run: | pip install --quiet semgrep set +e if [ -n "${SEMGREP_APP_TOKEN:-}" ]; then echo "Running Semgrep with managed policies..." semgrep ci --sarif --output=.sarif/semgrep.sarif else echo "Running Semgrep with OSS rules..." semgrep scan \ --config=auto \ --sarif \ --output=.sarif/semgrep.sarif \ --exclude='**/bin/**' \ --exclude='**/obj/**' \ --exclude='**/*.Tests.cs' \ --exclude='**/Mocks/**' \ --exclude='**/perf/**' \ --exclude='**/quantower/**' \ . fi rc=$? set -e if [ -f ".sarif/semgrep.sarif" ]; then echo "Semgrep SARIF generated (exit code: $rc):" ls -lh .sarif/semgrep.sarif else echo "WARNING: Semgrep SARIF not generated" fi - name: Upload SARIF artifact if: always() uses: actions/upload-artifact@v4 with: name: sarif-semgrep path: .sarif/semgrep.sarif retention-days: 7 if-no-files-found: warn # ============================================================================== # 4) Build, Test, Coverage, SonarCloud & Roslyn SARIF # ============================================================================== Sonar_Analysis: needs: [ReSharper_Analysis, Snyk_Scan, Semgrep_Scan] runs-on: ubuntu-latest timeout-minutes: 20 permissions: contents: read actions: write pull-requests: read checks: write steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 ref: ${{ env.CHECKOUT_REF }} - 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: Set up JDK 17 (for SonarCloud) uses: actions/setup-java@v4 with: java-version: 17 distribution: zulu - name: Install Tools run: | dotnet tool install --global dotnet-reportgenerator-globaltool dotnet tool install --global dotnet-sonarscanner echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH" - name: Restore dependencies run: dotnet restore - name: Check for SonarCloud token id: check_sonar env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | if [ -z "${SONAR_TOKEN:-}" ]; then echo "SONAR_TOKEN not set. Skipping SonarCloud." echo "skip=true" >> "$GITHUB_OUTPUT" else echo "skip=false" >> "$GITHUB_OUTPUT" fi - name: Prepare SARIF directory run: mkdir -p .sarif - name: Download External SARIFs uses: actions/download-artifact@v4 with: pattern: sarif-* path: .sarif merge-multiple: true - name: Build SARIF list for SonarCloud run: | # Keep SonarCloud focused on its native analyzers + security tools. # Importing compiler/analyzer SARIF (Roslyn/JetBrains) tends to explode "External issues". paths=() for f in .sarif/snyk.sarif .sarif/semgrep.sarif; do if [ -f "$f" ]; then paths+=("$f") fi done IFS=, echo "SONAR_SARIF_PATHS=${paths[*]}" >> "$GITHUB_ENV" - name: Begin SonarCloud Analysis if: steps.check_sonar.outputs.skip != 'true' env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: | args=( "/k:mihakralj_QuanTAlib" "/o:mihakralj-quantalib" "/d:sonar.token=$SONAR_TOKEN" "/d:sonar.host.url=https://sonarcloud.io" "/d:sonar.cs.opencover.reportsPaths=TestResults/**/coverage.opencover.xml" "/d:sonar.cs.vstest.reportsPaths=TestResults/**/*.trx" "/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" "/d:sonar.issue.ignore.multicriteria=e1" "/d:sonar.issue.ignore.multicriteria.e1.ruleKey=csharpsquid:S107" "/d:sonar.issue.ignore.multicriteria.e1.resourceKey=**/*" ) if [ -n "${SONAR_SARIF_PATHS:-}" ]; then args+=("/d:sonar.sarifReportPaths=$SONAR_SARIF_PATHS") fi dotnet sonarscanner begin "${args[@]}" - name: Build Solution with Roslyn SARIF run: | dotnet build QuanTAlib.sln \ --no-restore \ --configuration Debug \ --nologo \ -m:1 \ -p:TreatWarningsAsErrors=false \ -p:ErrorLog="$(pwd)/.sarif/roslyn.sarif;version=2.1" - name: Run Tests with Coverage run: | dotnet test QuanTAlib.sln \ --no-build \ --configuration Debug \ --collect:"XPlat Code Coverage;Format=opencover,cobertura,lcov" \ --results-directory ./TestResults \ --logger "trx;LogFileName=test_results.trx" - name: Merge Coverage Reports run: | mkdir -p coverage-merged reportgenerator \ "-reports:TestResults/**/coverage.opencover.xml;TestResults/**/coverage.cobertura.xml;TestResults/**/coverage.info" \ "-targetdir:coverage-merged" \ "-reporttypes:Cobertura;lcov" echo "Merged coverage outputs:" ls -la coverage-merged || true if [ ! -f "coverage-merged/Cobertura.xml" ]; then echo "ERROR: Cobertura.xml not generated (Codacy coverage will be missing)" exit 1 fi - name: End SonarCloud Analysis if: steps.check_sonar.outputs.skip != 'true' env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} run: dotnet sonarscanner end /d:sonar.token="$SONAR_TOKEN" - name: Upload Coverage Artifact uses: actions/upload-artifact@v4 with: name: coverage-reports path: coverage-merged/ retention-days: 7 - name: Upload Roslyn SARIF Artifact if: always() uses: actions/upload-artifact@v4 with: name: sarif-roslyn path: .sarif/roslyn.sarif retention-days: 7 if-no-files-found: warn # ============================================================================== # 5) Codacy Upload (SARIF + Coverage) # ============================================================================== Codacy_Upload: needs: [ReSharper_Analysis, Snyk_Scan, Semgrep_Scan, Sonar_Analysis] runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read actions: read if: always() steps: - name: Check for Codacy token id: check_token env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} run: | if [ -z "${CODACY_PROJECT_TOKEN:-}" ]; then echo "CODACY_PROJECT_TOKEN not set. Skipping Codacy uploads." echo "skip=true" >> "$GITHUB_OUTPUT" else echo "skip=false" >> "$GITHUB_OUTPUT" fi - name: Download SARIF artifacts if: steps.check_token.outputs.skip != 'true' uses: actions/download-artifact@v4 with: pattern: sarif-* path: sarif - name: Download Coverage Reports if: steps.check_token.outputs.skip != 'true' uses: actions/download-artifact@v4 with: name: coverage-reports path: coverage-merged - name: List artifacts if: steps.check_token.outputs.skip != 'true' run: | echo "SARIF files:" find sarif -name "*.sarif" -type f -maxdepth 4 -print -exec ls -lh {} \; || true echo "" echo "Coverage files:" ls -la coverage-merged || true - name: Install Codacy CLI v2 if: steps.check_token.outputs.skip != 'true' run: | set -euo pipefail # Install Codacy CLI using official bootstrap script echo "Installing Codacy CLI v2..." sudo curl -Ls https://raw.githubusercontent.com/codacy/codacy-cli-v2/main/codacy-cli.sh -o /usr/local/bin/codacy-cli sudo chmod +x /usr/local/bin/codacy-cli # Script will fetch binary if needed codacy-cli version - name: Upload SARIF files to Codacy if: steps.check_token.outputs.skip != 'true' env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} run: | set -euo pipefail shopt -s nullglob globstar files=(sarif/**/*.sarif) if [ ${#files[@]} -eq 0 ]; then echo "No SARIF files found. Nothing to upload." exit 0 fi echo "Uploading ${#files[@]} SARIF file(s) to Codacy with commit: ${COMMIT_UUID}" for sarif_file in "${files[@]}"; do echo "--- Uploading: $sarif_file ---" # Using short flags per codacy-cli-v2 docs: # -s: SARIF file path # -c: commit UUID # -t: project token codacy-cli upload \ -s "$sarif_file" \ -c "$COMMIT_UUID" \ -t "$CODACY_PROJECT_TOKEN" \ || echo "WARNING: failed to upload $sarif_file" done - name: Upload Coverage to Codacy if: steps.check_token.outputs.skip != 'true' env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} run: | set -euo pipefail curl -Ls https://coverage.codacy.com/get.sh -o codacy-coverage.sh chmod +x codacy-coverage.sh if [ -f "coverage-merged/Cobertura.xml" ]; then ./codacy-coverage.sh report -r "coverage-merged/Cobertura.xml" else echo "Cobertura.xml not found. Skipping coverage upload." fi