0

I'm currently building a CI/CD pipeline in GitLab, and I'm running into trouble with integrating a code coverage check.

What I’ve Got So Far:

A working GitLab Runner (on Windows).

An Apex class and a corresponding Apex test class, which locally produce 100% code coverage.

The pipeline already runs the tests successfully.

What I Need:

I'd like the pipeline to fail automatically if the code coverage drops below 80%. I want to enforce this threshold as part of our quality gate during the build process.

Where I’m Stuck:

I can’t seem to figure out:

How to retrieve and evaluate Apex code coverage from within the pipeline.

How to integrate that check into .gitlab-ci.yml.

Whether this requires any special Salesforce CLI commands, scripting, or third-party tools.

If anyone has implemented something similar or has insights on how to do this (especially in a GitLab + Windows + Apex context), I’d really appreciate your help.

Thanks in advance!

stages:
  - build-testing
  - prod-deploy

variables:
  SF_INSTANCE_URL: "https://test.salesforce.com"
  TEST_RESULTS_DIR: "artifacts/test-results"
  LOG_DIR: "artifacts/logs"

default:
  tags:
    - meh-playground
  before_script:
    - '$ErrorActionPreference = "Stop"'
    - 'Write-Host "Using PowerShell on Windows Runner"'
    - 'New-Item -ItemType Directory -Force -Path "assets","$Env:TEST_RESULTS_DIR","$Env:LOG_DIR" | Out-Null'
    # SERVER_KEY ist eine GitLab File-Variable → Pfad in $Env:SERVER_KEY
    - 'if (!(Test-Path $Env:SERVER_KEY)) { throw "SERVER_KEY File Variable not found!" }'
    - 'Copy-Item -Path $Env:SERVER_KEY -Destination "server.key"'
    # CLI bereitstellen + Übersicht
    - 'npx -y @salesforce/cli --version'
    - 'npx -y @salesforce/cli plugins --core'
  after_script:
    # Clean-up immer versuchen
    - 'if (Test-Path "server.key") { Remove-Item "server.key" -Force -ErrorAction SilentlyContinue }'

# --- Build & Test auf Scratch-Org ---
build-testing:
  stage: build-testing
  script:
    - |
      try {
        Write-Host "JWT login to $Env:SF_USERNAME_SANDBOX via $Env:SF_INSTANCE_URL"
        npx -y @salesforce/cli org login jwt `
          --instance-url "$Env:SF_INSTANCE_URL" `
          --client-id "$Env:SF_CONSUMER_KEY_SANDBOX" `
          --jwt-key-file "server.key" `
          --username "$Env:SF_USERNAME_SANDBOX" `
          --alias HubOrg `
          --set-default-dev-hub

        npx -y @salesforce/cli org whoami --target-org HubOrg

 
        npx -y @salesforce/cli org create scratch `
          --target-dev-hub HubOrg `
          --definition-file "config/project-scratch-def.json" `
          --alias ApexHours `
          --set-default `
          --wait 20 `
          --duration-days 7

        npx -y @salesforce/cli org display --target-org ApexHours

        # Metadaten deployen
        npx -y @salesforce/cli project deploy start `
          --target-org ApexHours `
          --source-dir "force-app" `
          --ignore-conflicts

        # Apex-Tests ausführen mit Coverage-Validierung
        Write-Host "Running Apex tests with code coverage validation (minimum 80%)"
        
        # JUnit-Format für GitLab Test Reports
        npx -y @salesforce/cli apex test run `
          --target-org ApexHours `
          --wait 30 `
          --result-format junit `
          --output-dir "$Env:TEST_RESULTS_DIR" `
          --loglevel warn

        # JSON-Format für Coverage-Analyse
        $testOutput = npx -y @salesforce/cli apex test run `
          --target-org ApexHours `
          --wait 30 `
          --result-format json `
          --loglevel warn

        # Test-Output in Datei speichern
        $testOutput | Out-File -FilePath "$Env:TEST_RESULTS_DIR\apex-tests.json" -Encoding UTF8

        # Coverage-Validierung
        try {
          $testJson = $testOutput | ConvertFrom-Json
          $orgWideCoverageStr = $testJson.result.summary.orgWideCoverage
          
          # Entferne das %-Zeichen und konvertiere zu Double
          if ($orgWideCoverageStr -match '(\d+(?:\.\d+)?)%?') {
            $coverage = [double]$matches[1]
            Write-Host "Org-wide coverage: $coverage%"
            
            if ($coverage -lt 80) {
              throw "Code coverage ($coverage%) is below the required minimum of 80%"
            } else {
              Write-Host "✓ Code coverage requirement met: $coverage% >= 80%"
            }
          } else {
            Write-Warning "Could not parse coverage value: $orgWideCoverageStr"
          }
        } catch {
          Write-Warning "Coverage validation failed: $($_.Exception.Message)"
          Write-Host "Test output for debugging:"
          Write-Host $testOutput
        }

        # (Optional) Org-Logs
        npx -y @salesforce/cli org display --target-org ApexHours | Out-File -FilePath "$Env:LOG_DIR\org-display.txt" -Encoding UTF8
      }
      finally {
        # Scratch-Org aufräumen, auch bei Fehlern
        try {
          npx -y @salesforce/cli org delete scratch --target-org ApexHours --no-prompt
        } catch {
          Write-Host "Scratch org cleanup skipped/failed."
        }
      }
  artifacts:
    when: always
    expire_in: 2 weeks
    reports:
      junit:
        - artifacts/test-results/**/*.xml
    paths:
      - artifacts/

# --- Manueller Deploy-Job ---
prod-deploy:
  stage: prod-deploy
  when: manual
  needs: ["build-testing"]
  resource_group: prod-deploy  
  script:
    - |
      Write-Host "JWT login to $Env:SF_USERNAME_SANDBOX via $Env:SF_INSTANCE_URL"
      npx -y @salesforce/cli org login jwt `
        --instance-url "$Env:SF_INSTANCE_URL" `
        --client-id "$Env:SF_CONSUMER_KEY_SANDBOX" `
        --jwt-key-file "server.key" `
        --username "$Env:SF_USERNAME_SANDBOX" `
        --alias HubOrg `
        --set-default-dev-hub

      # Hinweis: hier wird direkt in die angegebene Org deployed (aktuell: Sandbox/HubOrg)
      npx -y @salesforce/cli project deploy start `
        --target-org HubOrg `
        --source-dir "force-app/main/default" `
        --ignore-conflicts

I also tried this, but now he cant convert Apex to JSON:

stages:
  - build-testing
  - prod-deploy

variables:
  SF_INSTANCE_URL: "https://test.salesforce.com"

default:
  tags: [meh-playground]
  before_script:
    - '$ErrorActionPreference = "Stop"'
    - 'Write-Host "Using PowerShell on Windows Runner"'
    - 'if (!(Test-Path "assets")) { New-Item -ItemType Directory -Path "assets" | Out-Null }'
    - 'if (!(Test-Path $Env:SERVER_KEY)) { throw "SERVER_KEY File Variable not found!" }'
    - 'Copy-Item -Path $Env:SERVER_KEY -Destination "server.key"'
    - 'npx -y @salesforce/cli --version'
    - 'npx -y @salesforce/cli plugins --core'

build-testing:
  stage: build-testing
  script:
    - |
      # region PowerShell
      $ScratchAlias = if ($Env:CI_PIPELINE_ID) { "ApexHours_{0}" -f $Env:CI_PIPELINE_ID } elseif ($Env:CI_JOB_ID) { "ApexHours_{0}" -f $Env:CI_JOB_ID } else { "ApexHours_ci" }
      $ScratchCreated = $false

      try {
        Write-Host "JWT login to $Env:SF_USERNAME_SANDBOX via $Env:SF_INSTANCE_URL"
        & npx -y @salesforce/cli org login jwt `
          --instance-url "$Env:SF_INSTANCE_URL" `
          --client-id "$Env:SF_CONSUMER_KEY_SANDBOX" `
          --jwt-key-file "server.key" `
          --username "$Env:SF_USERNAME_SANDBOX" `
          --alias HubOrg `
          --set-default-dev-hub
        if ($LASTEXITCODE -ne 0) { throw "JWT login failed (exit $LASTEXITCODE)" }

        # --- Scratch-Org erstellen ---
        $createOut = (& npx -y @salesforce/cli org create scratch `
          --target-dev-hub HubOrg `
          --definition-file "config/project-scratch-def.json" `
          --alias $ScratchAlias `
          --set-default `
          --wait 10 `
          --duration-days 7 `
          --json) 2>&1
        $createCode = $LASTEXITCODE

        if ($createCode -ne 0) {
          $createTxt = $createOut | Out-String
          if ($createTxt -match 'LIMIT_EXCEEDED') {
            Write-Warning "Daily scratch org signup limit reached. Skipping deploy & tests for this run."
            # Wenn du hart fehlschlagen willst, ersetze die nächste Zeile durch: throw "Scratch org daily limit reached"
            exit 0
          } else {
            Write-Error $createTxt
            throw "Scratch org creation failed (exit $createCode)"
          }
        } else {
          $ScratchCreated = $true
        }

        if ($ScratchCreated) {
          & npx -y @salesforce/cli org display `
            --target-org $ScratchAlias
          if ($LASTEXITCODE -ne 0) { throw "org display failed (exit $LASTEXITCODE)" }

          & npx -y @salesforce/cli project deploy start `
            --target-org $ScratchAlias `
            --source-dir "force-app"
          if ($LASTEXITCODE -ne 0) { throw "project deploy failed (exit $LASTEXITCODE)" }

          $testResult = (& npx -y @salesforce/cli apex test run `
            --target-org $ScratchAlias `
            --wait 10 `
            --code-coverage `
            --json) 2>&1
          if ($LASTEXITCODE -ne 0) {
            Write-Error ($testResult | Out-String)
            throw "Apex test run failed (exit $LASTEXITCODE)"
          }

          if (-not $testResult) { throw "Apex test run produced no output." }
          $json = $testResult | ConvertFrom-Json
          if (-not $json -or -not $json.result) { throw "Failed to parse Apex test JSON." }

          $coverageItems = $json.result.codeCoverage
          if ($coverageItems) {
            $coverage = ($coverageItems | Measure-Object -Property coveragePercent -Average).Average
            Write-Host ("Average Code Coverage: {0}%" -f [math]::Round($coverage,2))
            if ($coverage -lt 80) { throw ("Code coverage is below 80%. Actual: {0}%" -f $coverage) }
          } else {
            Write-Warning "No code coverage details returned."
          }
        }
      }
      finally {
        try {
          if ($ScratchCreated) {
            & npx -y @salesforce/cli org delete scratch `
              --target-org $ScratchAlias `
              --no-prompt
          } else {
            Write-Host "No scratch org was created; skipping deletion."
          }
        } catch {
          Write-Host "Scratch org cleanup skipped/failed: $($_.Exception.Message)"
        }

        if (Test-Path "server.key") {
          Remove-Item "server.key" -Force -ErrorAction SilentlyContinue
        }
      }
      # endregion

prod-deploy:
  stage: prod-deploy
  when: manual
  needs: ["build-testing"]
  script:
    - 'Write-Host "JWT login to $Env:SF_USERNAME_SANDBOX via $Env:SF_INSTANCE_URL"'
    - |
      & npx -y @salesforce/cli org login jwt `
        --instance-url "$Env:SF_INSTANCE_URL" `
        --client-id "$Env:SF_CONSUMER_KEY_SANDBOX" `
        --jwt-key-file "server.key" `
        --username "$Env:SF_USERNAME_SANDBOX" `
        --alias HubOrg `
        --set-default-dev-hub
      if ($LASTEXITCODE -ne 0) { throw "JWT login failed (exit $LASTEXITCODE)" }
    - |
      & npx -y @salesforce/cli project deploy start `
        --target-org HubOrg `
        --source-dir "force-app/main/default"
      if ($LASTEXITCODE -ne 0) { throw "project deploy failed (exit $LASTEXITCODE)" }
  after_script:
    - 'if (Test-Path "server.key") { Remove-Item "server.key" -Force -ErrorAction SilentlyContinue }'

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.