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: contents: read 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: 20 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: Build solution run: dotnet build QuanTAlib.slnx --no-restore --configuration Debug --nologo -m:1 - 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 lib/quantalib.csproj \ --no-build \ --include="**/*.cs" \ --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 - name: Upload Roslyn SARIF artifacts if: always() uses: actions/upload-artifact@v4 with: name: sarif-roslyn path: | .sarif/*.sarif !.sarif/resharper.sarif retention-days: 7 if-no-files-found: warn # ============================================================================== # 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: Generate SonarCloud multicriteria from JSON id: sonar_rules run: | # Read rule IDs from sonar-suppressions.json and generate multicriteria args if [ ! -f ".github/sonar-suppressions.json" ]; then echo "ERROR: .github/sonar-suppressions.json not found" exit 1 fi # Extract rule IDs and build multicriteria parameters rules=$(jq -r '.rules[].id' .github/sonar-suppressions.json) multicriteria_names="" multicriteria_args="" index=1 for rule in $rules; do name="e${index}" if [ -n "$multicriteria_names" ]; then multicriteria_names="${multicriteria_names},${name}" else multicriteria_names="${name}" fi multicriteria_args="${multicriteria_args} /d:sonar.issue.ignore.multicriteria.${name}.ruleKey=csharpsquid:${rule} /d:sonar.issue.ignore.multicriteria.${name}.resourceKey=**/*" index=$((index + 1)) done echo "Generated multicriteria for $(echo "$rules" | wc -w) rules" echo "multicriteria_names=$multicriteria_names" >> "$GITHUB_OUTPUT" echo "multicriteria_args=$multicriteria_args" >> "$GITHUB_OUTPUT" - name: Begin SonarCloud Analysis if: steps.check_sonar.outputs.skip != 'true' env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} MULTICRITERIA_NAMES: ${{ steps.sonar_rules.outputs.multicriteria_names }} MULTICRITERIA_ARGS: ${{ steps.sonar_rules.outputs.multicriteria_args }} 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=$MULTICRITERIA_NAMES" ) # Add the dynamically generated multicriteria args eval "args+=($MULTICRITERIA_ARGS)" 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: | # Use the ErrorLog setting from Directory.Build.props which outputs # per-project SARIF files to .sarif/ directory with version=2.1 dotnet build QuanTAlib.slnx \ --no-restore \ --configuration Debug \ --nologo \ -m:1 \ -p:TreatWarningsAsErrors=false - name: Run Tests with Coverage run: | dotnet test QuanTAlib.slnx \ --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 # Roslyn SARIF artifacts are uploaded by ReSharper_Analysis job # (which builds without SonarScanner wrapper that can suppress ErrorLog output) # ============================================================================== # 5) GitHub Code Scanning Upload (SARIF → Security tab) # ============================================================================== GitHub_Security_Upload: needs: [ReSharper_Analysis, Snyk_Scan, Semgrep_Scan, Sonar_Analysis] runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read actions: read security-events: write if: always() steps: - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ env.CHECKOUT_REF }} - name: Download all SARIF artifacts uses: actions/download-artifact@v4 with: pattern: sarif-* path: sarif merge-multiple: true - name: List SARIF files run: | echo "Downloaded SARIF files:" find sarif -name "*.sarif" -o -name "*.sarif.json" 2>/dev/null | head -50 || true ls -la sarif/ || true - name: Split multi-run SARIF files run: | # CodeQL Action v4 requires single-run SARIF files per category mkdir -p sarif-split for sarif_file in sarif/*.sarif; do [ -f "$sarif_file" ] || continue basename=$(basename "$sarif_file" .sarif) echo "Processing $sarif_file..." # Count runs in the SARIF file run_count=$(jq '.runs | length' "$sarif_file" 2>/dev/null || echo "0") if [ "$run_count" -le 1 ]; then # Single run or empty - copy as-is cp "$sarif_file" "sarif-split/${basename}.sarif" echo " Copied (${run_count} run)" else # Multiple runs - split into separate files echo " Splitting into $run_count separate files" for i in $(seq 0 $((run_count - 1))); do jq --argjson idx "$i" '{ "$schema": ."$schema", "version": .version, "runs": [.runs[$idx]] }' "$sarif_file" > "sarif-split/${basename}-run${i}.sarif" echo " Created sarif-split/${basename}-run${i}.sarif" done fi done echo "" echo "Processed SARIF files:" ls -la sarif-split/ || true - name: Upload SARIF files to GitHub Security if: always() run: | # Upload each split SARIF file with a unique category for sarif_file in sarif-split/*.sarif; do [ -f "$sarif_file" ] || continue basename=$(basename "$sarif_file" .sarif) echo "Uploading $sarif_file with category: $basename" # Use the GitHub CLI to upload SARIF (codeql-action/upload-sarif doesn't support dynamic categories well) # Fall back to individual action calls via workflow commands done - name: Upload ReSharper SARIF to GitHub Security if: always() && hashFiles('sarif-split/resharper.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/resharper.sarif category: resharper continue-on-error: true - name: Upload ReSharper SARIF runs to GitHub Security if: always() run: | # Upload any split ReSharper runs for sarif_file in sarif-split/resharper-run*.sarif; do [ -f "$sarif_file" ] || continue echo "Note: Additional ReSharper run file exists: $sarif_file" echo "These will be uploaded in subsequent steps if needed" done continue-on-error: true - name: Upload Snyk SARIF to GitHub Security if: always() && hashFiles('sarif-split/snyk.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/snyk.sarif category: snyk continue-on-error: true - name: Upload Semgrep SARIF to GitHub Security if: always() && hashFiles('sarif-split/semgrep.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/semgrep.sarif category: semgrep continue-on-error: true # Upload per-project Roslyn SARIF files (generated by Directory.Build.props) - name: Upload quantalib SARIF to GitHub Security if: always() && hashFiles('sarif-split/quantalib.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/quantalib.sarif category: roslyn-quantalib continue-on-error: true - name: Upload QuanTAlib.Tests SARIF to GitHub Security if: always() && hashFiles('sarif-split/QuanTAlib.Tests.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/QuanTAlib.Tests.sarif category: roslyn-tests continue-on-error: true - name: Upload Quantower.Tests SARIF to GitHub Security if: always() && hashFiles('sarif-split/Quantower.Tests.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/Quantower.Tests.sarif category: roslyn-quantower-tests continue-on-error: true # Upload split runs with unique categories (run0, run1, etc.) - name: Upload ReSharper split run 0 if: always() && hashFiles('sarif-split/resharper-run0.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/resharper-run0.sarif category: resharper-run0 continue-on-error: true - name: Upload ReSharper split run 1 if: always() && hashFiles('sarif-split/resharper-run1.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/resharper-run1.sarif category: resharper-run1 continue-on-error: true - name: Upload Snyk split run 0 if: always() && hashFiles('sarif-split/snyk-run0.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/snyk-run0.sarif category: snyk-run0 continue-on-error: true - name: Upload Snyk split run 1 if: always() && hashFiles('sarif-split/snyk-run1.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/snyk-run1.sarif category: snyk-run1 continue-on-error: true - name: Upload Roslyn split run 0 if: always() && hashFiles('sarif-split/roslyn-run0.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/roslyn-run0.sarif category: roslyn-run0 continue-on-error: true - name: Upload Roslyn split run 1 if: always() && hashFiles('sarif-split/roslyn-run1.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/roslyn-run1.sarif category: roslyn-run1 continue-on-error: true - name: Upload Roslyn split run 2 if: always() && hashFiles('sarif-split/roslyn-run2.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/roslyn-run2.sarif category: roslyn-run2 continue-on-error: true - name: Upload Roslyn split run 3 if: always() && hashFiles('sarif-split/roslyn-run3.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/roslyn-run3.sarif category: roslyn-run3 continue-on-error: true - name: Upload Roslyn split run 4 if: always() && hashFiles('sarif-split/roslyn-run4.sarif') != '' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: sarif-split/roslyn-run4.sarif category: roslyn-run4 continue-on-error: true # ============================================================================== # 6) DeepSource Coverage Upload # ============================================================================== DeepSource_Upload: needs: [Sonar_Analysis] runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: read actions: read if: always() steps: - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ env.CHECKOUT_REF }} - name: Download Coverage Reports uses: actions/download-artifact@v4 with: name: coverage-reports path: coverage-merged - name: Upload Coverage to DeepSource env: DEEPSOURCE_DSN: ${{ secrets.DEEPSOURCE_DSN }} run: | if [ -z "${DEEPSOURCE_DSN:-}" ]; then echo "DEEPSOURCE_DSN not set. Skipping DeepSource upload." exit 0 fi curl https://deepsource.io/cli | sh if [ -f "coverage-merged/Cobertura.xml" ]; then ./bin/deepsource report --analyzer test-coverage --key csharp --value-file coverage-merged/Cobertura.xml else echo "Cobertura.xml not found. Skipping coverage upload." fi # ============================================================================== # 7) Codacy Rule Sync (disable Sonar rules from sonar-suppressions.json) # ============================================================================== Codacy_Rule_Sync: runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: read steps: - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ env.CHECKOUT_REF }} - name: Check for Codacy API token id: check_api_token env: CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }} run: | if [ -z "${CODACY_API_TOKEN:-}" ]; then echo "CODACY_API_TOKEN not set. Skipping Codacy rule sync." echo "To enable automatic rule sync, create a CODACY_API_TOKEN secret" echo "with an API token from https://app.codacy.com/account/apiTokens" echo "skip=true" >> "$GITHUB_OUTPUT" else echo "skip=false" >> "$GITHUB_OUTPUT" fi - name: Sync Sonar rules to Codacy if: steps.check_api_token.outputs.skip != 'true' env: CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }} run: | set -euo pipefail # Configuration PROVIDER="gh" ORGANIZATION="mihakralj" REPOSITORY="QuanTAlib" TOOL_UUID="8954dff3-f19c-429c-ac76-c45fa5e73b62" # SonarC# tool UUID API_BASE="https://app.codacy.com/api/v3" if [ ! -f ".github/sonar-suppressions.json" ]; then echo "ERROR: .github/sonar-suppressions.json not found" exit 1 fi # Extract rule IDs from JSON rules=$(jq -r '.rules[].id' .github/sonar-suppressions.json) rule_count=$(echo "$rules" | wc -w) echo "Found $rule_count rules to disable in Codacy" # Function to disable a pattern disable_pattern() { local pattern_id="$1" local response echo "Disabling pattern: $pattern_id" response=$(curl -s -w "\n%{http_code}" -X PATCH \ "${API_BASE}/organizations/${PROVIDER}/${ORGANIZATION}/repositories/${REPOSITORY}/tools/${TOOL_UUID}/patterns/${pattern_id}" \ -H "api-token: ${CODACY_API_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"enabled": false}') http_code=$(echo "$response" | tail -1) body=$(echo "$response" | head -n -1) if [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; then echo " ✓ Successfully disabled $pattern_id" return 0 elif [ "$http_code" = "404" ]; then echo " ⚠ Pattern $pattern_id not found (may not exist in this tool)" return 0 else echo " ✗ Failed to disable $pattern_id (HTTP $http_code)" echo " Response: $body" return 1 fi } # Process each rule success_count=0 fail_count=0 for rule in $rules; do if disable_pattern "$rule"; then success_count=$((success_count + 1)) else fail_count=$((fail_count + 1)) fi done echo "" echo "=== Codacy Rule Sync Summary ===" echo "Total rules: $rule_count" echo "Successful: $success_count" echo "Failed: $fail_count" if [ $fail_count -gt 0 ]; then echo "" echo "Some rules failed to sync. This may be due to:" echo "- Pattern IDs not matching Codacy's internal naming" echo "- API rate limiting" echo "- Permission issues with the API token" echo "" echo "Consider manually verifying rules at:" echo "https://app.codacy.com/gh/${ORGANIZATION}/${REPOSITORY}/patterns" fi # ============================================================================== # 8) Codacy Upload (SARIF + Coverage) # ============================================================================== Codacy_Upload: needs: [ReSharper_Analysis, Snyk_Scan, Semgrep_Scan, Sonar_Analysis, Codacy_Rule_Sync] 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