Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
3D render of code quality metrics showing a healthy graph, illustrating Salesforce code quality.
DevOps

Salesforce Code Quality: Automated PR Comments

Automate static analysis on every pull request and post the findings as PR comments. Developers get the feedback while the code is still open for change, which keeps technical debt from piling up.

Key takeaways Run Salesforce Code Analyzer, PMD and ESLint inside the CI/CD pipeline rather than leaning on peer review. Post the findings as pull request comments so developers see them where they are working. Shift left, and catch the issues early in the development lifecycle. Agree coding standards and violation thresholds first, and give the pipeline identity the repository permissions it needs. Normalize file paths and build the API request correctly, or the comments land on the wrong code lines.

Once a Salesforce org is big enough, manual peer review stops keeping up with the technical debt and security risk across Apex, Lightning Web Components (LWCs) and a lot of metadata. Static code analysis (SCA) closes that gap with automated quality gates. Wire Salesforce Code Analyzer, PMD and ESLint into a CI/CD pipeline such as Azure DevOps and you have the "shift-left" pattern. Post the findings as pull request (PR) comments and the developer reads them while the code is still open for change.

The tools that read Salesforce code

Static analysis only works if the engine can parse the language in front of it. PMD has been the Apex standard for years, catching SOQL queries inside loops and high cyclomatic complexity. The current stack also has LWCs and Aura components, which need broader analysis than PMD gives on its own.

Salesforce Code Analyzer is the official, Salesforce-supported tool that unifies several engines. It runs PMD for Apex and ESLint for LWCs and Aura behind one interface, which keeps version management simple and the rule sets in line with Salesforce best practices.

This guide assumes you know your way around Azure DevOps. If not, start with the Azure DevOps Pipelines documentation.

Feedback only helps if it lands where the developer is working. Line-level comments on the PR spare them the trip through the build log. Add branch policies on top and only compliant code merges.

Repository structure

An Azure DevOps pipeline for Salesforce usually reads a azure-pipelines.yml file at the project root. A common layout:

my-salesforce-project/
├── force-app/
│   └── main/
│       └── default/
│           ├── classes/
│           ├── triggers/
│           └── lwc/
├── azure-pipelines.yml
└── pmd-rules/ (optional)
    └── apex-ruleset.xml

Azure DevOps configuration

Set the pipeline up as follows.

Enable the system access token

In Azure DevOps, open Pipeline Settings, check Allow scripts to access the OAuth token and save.

Set branch policies

Go to Repos, then Branches. Select your main branch and open Branch policies. Add a Build validation requirement and point it at your static analysis pipeline.

Agent requirements

Pick an agent pool. ubuntu-latest is the recommendation for Microsoft-hosted builds.

Ruleset definition

Define custom ruleset XML files such as apex-ruleset.xml to say which PMD rules apply.

Violation thresholds

Set objective pass/fail criteria. Fail the build on any critical security issue, for instance, but allow a limited number of minor warnings.

Artifact storage strategy

Publish the scan reports (XML, JSON or HTML) as pipeline artifacts for later audit and review.

Organizational prerequisites

Some of this is governance, not YAML. Settle it before you build the pipeline:

  • Documented coding standards, so the scanner configuration matches the internal security and maintainability policy it enforces.
  • Defined violation thresholds. Critical security violations fail the build, for instance.
  • Repository access control. The pipeline's service identity needs "Contribute to pull requests" permissions in Azure Repos to create automated threads.
  • Ruleset definition. Keep rulesets such as apex-ruleset.xml in version control so local development and the pipeline check the same things.

Pipeline flow

The steps run in this order:

  1. PR context validation. Checks that the pipeline is running inside a pull request.
  2. Scanner execution. Invokes the Salesforce Code Analyzer engine.
  3. Results processing. Parses the JSON output to count violations.
  4. API configuration. Builds the connection to the Azure DevOps API.
  5. Path normalization. Aligns file paths between the agent and the repository.
  6. Comment generation. Creates the feedback strings developers will read.
  7. Guidance logic. Adds rule-specific "Quick Fix" suggestions.
  8. API request construction. Packages the comments into the PR diff view.
  9. Error handling. Manages API rate limits and connection retries.

The script, step by step

These snippets are the parts of the azure-pipelines.yml that carry the work.

Step 1: PR context validation

Check for the SYSTEM_PULLREQUEST_PULLREQUESTID environment variable and exit early when it is missing, so no scan runs outside a PR.

if (!$env:SYSTEM_PULLREQUEST_PULLREQUESTID) {
  Write-Host "Not in PR context"
  exit 0
}

Step 2: PMD scanner execution

This runs the Salesforce CLI scanner against the force-app directory and writes the results to a JSON file.

cmd /c "sfdx scanner:run --target force-app --engine pmd --format json --outfile $outputFile 2>&1" | Out-Null

Step 3: Results processing and validation

Parse the JSON output and count the violations. -Raw reads the whole file as one string so it converts cleanly into a PowerShell object.

$jsonContent = Get-Content $outputFile -Raw
$results = $jsonContent | ConvertFrom-Json

$totalViolations = 0
foreach ($file in $results) {
  if ($file.violations) {
    $totalViolations += $file.violations.Count
  }
}

Step 4: Azure DevOps API configuration

Build the API endpoint URL from the predefined Azure DevOps variables.

$org = $env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI.TrimEnd('/')
$project = $env:SYSTEM_TEAMPROJECT
$repoId = $env:BUILD_REPOSITORY_ID
$prId = $env:SYSTEM_PULLREQUEST_PULLREQUESTID

$baseUrl = "$org/$project/_apis/git/repositories/$repoId/pullRequests/$prId"

Step 5: File path normalization

Normalize the file paths so the comments land on the right lines in the Azure DevOps PR UI.

if ($filePath -match '.*\(force-app\.*)') {
  $filePath = $Matches[1] -replace '\', '/'
} elseif ($filePath -match '.*(force-app\.*)') {
  $filePath = $Matches[1] -replace '\', '/'
} else {
  $workingDir = Get-Location
  $filePath = $filePath -replace [regex]::Escape($workingDir), '' -replace '^\', '' -replace '\', '/'
}

Step 6: Comment generation

Assemble the comment text, with the severity and the issue description.

$comment = "$severityIcon $cleanRuleName`n`n"
$comment += "File: $filePath`n"
$comment += "Line: $($violation.line)"

if ($violation.column) {
  $comment += ", Column: $($violation.column)"
}
$comment += "`n`n"

$comment += "Issue: $($violation.message.Trim())`n`n"

if ($violation.category) {
  $comment += "Category: $($violation.category)`n"
}

$priorityText = switch ($violation.severity) {
  1 { "Critical - Fix Immediately" }
  2 { "Important - Should Fix" }
  3 { "Suggestion - Consider Fixing" }
  default { "Info" }
}
$comment += "Priority: $priorityText`n"

Step 7: Rule-specific fix suggestions

Attach targeted advice for the common violation types.

switch -Wildcard ($cleanRuleName) {
  "*SOQL*" {
    $comment += "- Move SOQL queries outside of loops`n"
    $comment += "- Use bulk operations and collections`n"
    $comment += "- Consider using Map<Id, SObject> for efficient lookups"
  }
  "*Security*" {
    $comment += "- Validate user permissions before SOQL/DML operations`n"
    $comment += "- Use 'WITH SECURITY_ENFORCED' in SOQL queries`n"
    $comment += "- Escape user input to prevent injection attacks"
  }
  # Additional patterns...
}

Step 8: API request construction

Build the Azure DevOps API request body, with the thread context that pins each comment to a line.

$commentBody = @{
  comments = @(
    @{ 
      parentCommentId = 0
      content = $comment
      commentType = 1
    }
  )
  threadContext = @{
    filePath = $filePath
    rightFileStart = @{
      line = $startLine
      offset = 1
    }
    rightFileEnd = @{
      line = $endLine
      offset = 1
    }
  }
}

Originally reported by salesforceben.com

Newsletter

One email every Tuesday

New guides, tool updates, and the release-note changes that break things.

No spam. Unsubscribe in one click.

Comments

Loading comments...

Leave a Comment